first commit
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"java.configuration.updateBuildConfiguration": "automatic"
|
"java.configuration.updateBuildConfiguration": "automatic",
|
||||||
|
"cmake.sourceDirectory": "E:/JOKI/smartinfuse/linux"
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Permissions untuk notifications -->
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="smartinfuse"
|
android:label="SMART INFUSE"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -1,14 +1,21 @@
|
||||||
class ChartDataPoint {
|
class ChartDataPoint {
|
||||||
final int hour;
|
final DateTime timestamp;
|
||||||
final double dropsPerMinute;
|
final double dropsPerMinute;
|
||||||
|
|
||||||
ChartDataPoint({
|
ChartDataPoint({
|
||||||
required this.hour,
|
required this.timestamp,
|
||||||
required this.dropsPerMinute,
|
required this.dropsPerMinute,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Label waktu format HH:mm, dipakai di sumbu X grafik & tooltip.
|
||||||
|
String get timeLabel {
|
||||||
|
final h = timestamp.hour.toString().padLeft(2, '0');
|
||||||
|
final m = timestamp.minute.toString().padLeft(2, '0');
|
||||||
|
return '$h:$m';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ChartDataPoint(hour: $hour, dropsPerMinute: $dropsPerMinute)';
|
return 'ChartDataPoint(timestamp: $timestamp, dropsPerMinute: $dropsPerMinute)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// lib/app/models/realtime_monitoring_model.dart
|
|
||||||
|
|
||||||
class RealtimeMonitoringModel {
|
class RealtimeMonitoringModel {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String roomId;
|
final String roomId;
|
||||||
|
|
@ -10,6 +8,7 @@ class RealtimeMonitoringModel {
|
||||||
final DateTime lastUpdate;
|
final DateTime lastUpdate;
|
||||||
final int servoAngleOpen;
|
final int servoAngleOpen;
|
||||||
final int servoAngleClose;
|
final int servoAngleClose;
|
||||||
|
final bool buzzerActive; // ← tambah ini
|
||||||
|
|
||||||
RealtimeMonitoringModel({
|
RealtimeMonitoringModel({
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
|
|
@ -21,9 +20,9 @@ class RealtimeMonitoringModel {
|
||||||
required this.lastUpdate,
|
required this.lastUpdate,
|
||||||
this.servoAngleOpen = 90,
|
this.servoAngleOpen = 90,
|
||||||
this.servoAngleClose = 0,
|
this.servoAngleClose = 0,
|
||||||
|
this.buzzerActive = false, // ← tambah ini
|
||||||
});
|
});
|
||||||
|
|
||||||
// From Realtime Database
|
|
||||||
factory RealtimeMonitoringModel.fromRealtimeDB(
|
factory RealtimeMonitoringModel.fromRealtimeDB(
|
||||||
String deviceId,
|
String deviceId,
|
||||||
String roomId,
|
String roomId,
|
||||||
|
|
@ -37,6 +36,7 @@ class RealtimeMonitoringModel {
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
servoOpen: controlling['servo_open'] ?? false,
|
servoOpen: controlling['servo_open'] ?? false,
|
||||||
|
buzzerActive: controlling['buzzer_active'] ?? false, // ← tambah ini
|
||||||
lastCommand: DateTime.parse(
|
lastCommand: DateTime.parse(
|
||||||
controlling['last_command'] ?? DateTime.now().toIso8601String(),
|
controlling['last_command'] ?? DateTime.now().toIso8601String(),
|
||||||
),
|
),
|
||||||
|
|
@ -50,11 +50,11 @@ class RealtimeMonitoringModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// To Realtime Database
|
|
||||||
Map<String, dynamic> toRealtimeDB() {
|
Map<String, dynamic> toRealtimeDB() {
|
||||||
return {
|
return {
|
||||||
'controlling': {
|
'controlling': {
|
||||||
'servo_open': servoOpen,
|
'servo_open': servoOpen,
|
||||||
|
'buzzer_active': buzzerActive, // ← tambah ini
|
||||||
'last_command': lastCommand.toIso8601String(),
|
'last_command': lastCommand.toIso8601String(),
|
||||||
},
|
},
|
||||||
'monitoring': {
|
'monitoring': {
|
||||||
|
|
@ -73,6 +73,7 @@ class RealtimeMonitoringModel {
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? roomId,
|
String? roomId,
|
||||||
bool? servoOpen,
|
bool? servoOpen,
|
||||||
|
bool? buzzerActive, // ← tambah ini
|
||||||
DateTime? lastCommand,
|
DateTime? lastCommand,
|
||||||
String? deviceStatus,
|
String? deviceStatus,
|
||||||
double? dropRate,
|
double? dropRate,
|
||||||
|
|
@ -84,6 +85,7 @@ class RealtimeMonitoringModel {
|
||||||
deviceId: deviceId ?? this.deviceId,
|
deviceId: deviceId ?? this.deviceId,
|
||||||
roomId: roomId ?? this.roomId,
|
roomId: roomId ?? this.roomId,
|
||||||
servoOpen: servoOpen ?? this.servoOpen,
|
servoOpen: servoOpen ?? this.servoOpen,
|
||||||
|
buzzerActive: buzzerActive ?? this.buzzerActive, // ← tambah ini
|
||||||
lastCommand: lastCommand ?? this.lastCommand,
|
lastCommand: lastCommand ?? this.lastCommand,
|
||||||
deviceStatus: deviceStatus ?? this.deviceStatus,
|
deviceStatus: deviceStatus ?? this.deviceStatus,
|
||||||
dropRate: dropRate ?? this.dropRate,
|
dropRate: dropRate ?? this.dropRate,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class DeviceDetailController extends GetxController {
|
||||||
final isServoActive = false.obs;
|
final isServoActive = false.obs;
|
||||||
final deviceStatus = 'disconnected'.obs;
|
final deviceStatus = 'disconnected'.obs;
|
||||||
final isLoading = true.obs;
|
final isLoading = true.obs;
|
||||||
|
final isBuzzerActive = false.obs;
|
||||||
final RxList<ChartDataPoint> chartData = <ChartDataPoint>[].obs;
|
final RxList<ChartDataPoint> chartData = <ChartDataPoint>[].obs;
|
||||||
|
|
||||||
StreamSubscription? _realtimeSubscription;
|
StreamSubscription? _realtimeSubscription;
|
||||||
|
|
@ -117,6 +117,7 @@ class DeviceDetailController extends GetxController {
|
||||||
lastUpdate.value = data.lastUpdate;
|
lastUpdate.value = data.lastUpdate;
|
||||||
isServoActive.value = data.servoOpen;
|
isServoActive.value = data.servoOpen;
|
||||||
deviceStatus.value = data.deviceStatus;
|
deviceStatus.value = data.deviceStatus;
|
||||||
|
isBuzzerActive.value = data.buzzerActive;
|
||||||
}
|
}
|
||||||
}, onError: (error) => AppSnackbar.error('Koneksi realtime terputus'));
|
}, onError: (error) => AppSnackbar.error('Koneksi realtime terputus'));
|
||||||
}
|
}
|
||||||
|
|
@ -140,15 +141,16 @@ class DeviceDetailController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _processHistoryData(List<HistoryModel> histories) {
|
void _processHistoryData(List<HistoryModel> histories) {
|
||||||
|
// Ambil 10 data TERBARU
|
||||||
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
|
||||||
final latestHistories = histories.take(10).toList();
|
final latestHistories = histories.take(10).toList();
|
||||||
|
|
||||||
|
// Urutkan lagi dari lama -> baru supaya grafik terbaca kiri ke kanan
|
||||||
latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
|
||||||
final List<ChartDataPoint> points = latestHistories.map((history) {
|
final List<ChartDataPoint> points = latestHistories.map((history) {
|
||||||
return ChartDataPoint(
|
return ChartDataPoint(
|
||||||
hour: history.timestamp.hour,
|
timestamp: history.timestamp,
|
||||||
dropsPerMinute: history.dropPerMinute,
|
dropsPerMinute: history.dropPerMinute,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
@ -186,7 +188,18 @@ class DeviceDetailController extends GetxController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> turnOffBuzzer() async {
|
||||||
|
try {
|
||||||
|
final dbRoomId = roomId!.toLowerCase().replaceAll(' ', '_');
|
||||||
|
await _realtimeService.controlBuzzer(dbRoomId, deviceId!, false);
|
||||||
|
isBuzzerActive.value = false;
|
||||||
|
AppSnackbar.success('Buzzer dimatikan');
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.error('Gagal mematikan buzzer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refreshData() async {
|
Future<void> refreshData() async {
|
||||||
await _loadDeviceData();
|
await _loadDeviceData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,15 +49,24 @@ class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||||
children: [
|
children: [
|
||||||
_buildSection(
|
_buildSection(
|
||||||
title: 'Monitoring Infus',
|
title: 'Monitoring Infus',
|
||||||
onSeeAll: () {},
|
|
||||||
child: _buildMonitoringCard(),
|
child: _buildMonitoringCard(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildSection(
|
_buildSection(
|
||||||
title: 'Kontrol Alat',
|
title: 'Kontrol Alat',
|
||||||
onSeeAll: () {},
|
|
||||||
child: _buildControlCard(),
|
child: _buildControlCard(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Buzzer card — hanya tampil kalau buzzer aktif
|
||||||
|
Obx(() {
|
||||||
|
if (!controller.isBuzzerActive.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: _buildBuzzerCard(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildSection(
|
_buildSection(
|
||||||
title: 'History Data',
|
title: 'History Data',
|
||||||
|
|
@ -100,19 +109,7 @@ class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onSeeAll != null)
|
|
||||||
GestureDetector(
|
|
||||||
onTap: onSeeAll,
|
|
||||||
child: const Text(
|
|
||||||
'See all',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -335,6 +332,98 @@ class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Card buzzer — muncul saat buzzer_active == true
|
||||||
|
Widget _buildBuzzerCard() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[50],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.red[200]!),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.red.withOpacity(0.08),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Icon dengan animasi pulse
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[100],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.notifications_active_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: Colors.red[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Buzzer Aktif',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.red[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Tetesan infus berhenti. Harap periksa pasien.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.red[600],
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Tombol matikan buzzer
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.turnOffBuzzer,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red[700],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.volume_off_rounded, size: 18),
|
||||||
|
SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
'Matikan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEmptyChart() {
|
Widget _buildEmptyChart() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
|
|
@ -375,4 +464,4 @@ class DeviceDetailView extends GetView<DeviceDetailController> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../../../services/firestore_service.dart';
|
import '../../../services/firestore_service.dart';
|
||||||
import '../../../services/realtime_database_service.dart';
|
import '../../../services/realtime_database_service.dart';
|
||||||
|
import '../../../services/notification_service.dart';
|
||||||
import '../../../models/patient_model.dart';
|
import '../../../models/patient_model.dart';
|
||||||
import '../../../models/chart_data_point.dart';
|
import '../../../models/chart_data_point.dart';
|
||||||
import '../../../models/history_model.dart';
|
import '../../../models/history_model.dart';
|
||||||
|
|
@ -51,6 +52,7 @@ class NotificationData {
|
||||||
class HomePatientController extends GetxController {
|
class HomePatientController extends GetxController {
|
||||||
final FirestoreService _firestoreService = FirestoreService();
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||||
|
final NotificationService _notificationService = NotificationService();
|
||||||
|
|
||||||
final deviceId = ''.obs;
|
final deviceId = ''.obs;
|
||||||
final roomId = ''.obs;
|
final roomId = ''.obs;
|
||||||
|
|
@ -71,6 +73,8 @@ class HomePatientController extends GetxController {
|
||||||
deviceStatus: 'disconnected',
|
deviceStatus: 'disconnected',
|
||||||
).obs;
|
).obs;
|
||||||
|
|
||||||
|
final isBuzzerActive = false.obs;
|
||||||
|
|
||||||
final chartDataPoints = <ChartDataPoint>[].obs;
|
final chartDataPoints = <ChartDataPoint>[].obs;
|
||||||
final notificationsList = <NotificationData>[].obs;
|
final notificationsList = <NotificationData>[].obs;
|
||||||
|
|
||||||
|
|
@ -82,9 +86,12 @@ class HomePatientController extends GetxController {
|
||||||
StreamSubscription? _notificationsSubscription;
|
StreamSubscription? _notificationsSubscription;
|
||||||
StreamSubscription? _historySubscription;
|
StreamSubscription? _historySubscription;
|
||||||
|
|
||||||
|
final Set<String> _alertedDevices = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
_initializeNotificationService();
|
||||||
_loadDataFromArguments();
|
_loadDataFromArguments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +108,15 @@ class HomePatientController extends GetxController {
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeNotificationService() async {
|
||||||
|
try {
|
||||||
|
await _notificationService.initialize();
|
||||||
|
print('Notification service initialized');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error initializing notification service: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _loadDataFromArguments() async {
|
void _loadDataFromArguments() async {
|
||||||
try {
|
try {
|
||||||
final args = Get.arguments as Map<String, dynamic>?;
|
final args = Get.arguments as Map<String, dynamic>?;
|
||||||
|
|
@ -176,14 +192,16 @@ class HomePatientController extends GetxController {
|
||||||
|
|
||||||
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
||||||
|
|
||||||
_realtimeSubscription?.cancel(); // Cancel previous subscription if exists
|
_realtimeSubscription?.cancel();
|
||||||
|
|
||||||
_realtimeSubscription = _realtimeService
|
_realtimeSubscription = _realtimeService
|
||||||
.getDeviceStream(dbRoomId, deviceId.value)
|
.getDeviceStream(dbRoomId, deviceId.value)
|
||||||
.listen(
|
.listen(
|
||||||
(device) {
|
(device) async {
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
_updateCurrentInfusData(device);
|
await _updateCurrentInfusData(device);
|
||||||
|
await _checkDropRate(device);
|
||||||
|
isBuzzerActive.value = device.buzzerActive;
|
||||||
} else {
|
} else {
|
||||||
print('Received null device data');
|
print('Received null device data');
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +218,7 @@ class HomePatientController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCurrentInfusData(RealtimeMonitoringModel device) async {
|
Future<void> _updateCurrentInfusData(RealtimeMonitoringModel device) async {
|
||||||
try {
|
try {
|
||||||
final room = await _firestoreService.getRoomById(device.roomId);
|
final room = await _firestoreService.getRoomById(device.roomId);
|
||||||
|
|
||||||
|
|
@ -217,11 +235,61 @@ class HomePatientController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkDropRate(RealtimeMonitoringModel device) async {
|
||||||
|
try {
|
||||||
|
final deviceId = device.deviceId;
|
||||||
|
final dropRate = device.dropRate;
|
||||||
|
final deviceStatus = device.deviceStatus;
|
||||||
|
|
||||||
|
if (dropRate == 0.0 && deviceStatus == 'connected') {
|
||||||
|
if (!_alertedDevices.contains(deviceId)) {
|
||||||
|
final patient = await _firestoreService.getPatientByDeviceId(
|
||||||
|
deviceId,
|
||||||
|
);
|
||||||
|
final room = await _firestoreService.getRoomById(device.roomId);
|
||||||
|
|
||||||
|
if (patient != null) {
|
||||||
|
await _notificationService.showDropRateAlertForPatient(
|
||||||
|
patientId: patient.id,
|
||||||
|
patientName: patient.namePatient,
|
||||||
|
deviceId: deviceId,
|
||||||
|
roomName: room?.roomName ?? device.roomId,
|
||||||
|
);
|
||||||
|
|
||||||
|
_alertedDevices.add(deviceId);
|
||||||
|
print('Alert sent for device: $deviceId');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (dropRate > 0.0) {
|
||||||
|
if (_alertedDevices.contains(deviceId)) {
|
||||||
|
_alertedDevices.remove(deviceId);
|
||||||
|
_notificationService.clearCooldown(deviceId);
|
||||||
|
await _notificationService.cancelNotificationByDeviceId(deviceId);
|
||||||
|
print('Device reset and notification dismissed: $deviceId');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking drop rate: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> turnOffBuzzer() async {
|
||||||
|
try {
|
||||||
|
final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_');
|
||||||
|
await _realtimeService.controlBuzzer(dbRoomId, deviceId.value, false);
|
||||||
|
isBuzzerActive.value = false;
|
||||||
|
AppSnackbar.success('Buzzer dimatikan');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error turning off buzzer: $e');
|
||||||
|
AppSnackbar.error('Gagal mematikan buzzer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _loadHistoryData() {
|
void _loadHistoryData() {
|
||||||
try {
|
try {
|
||||||
isLoadingHistory.value = true;
|
isLoadingHistory.value = true;
|
||||||
|
|
||||||
_historySubscription?.cancel(); // Cancel previous subscription if exists
|
_historySubscription?.cancel();
|
||||||
|
|
||||||
_historySubscription = _firestoreService
|
_historySubscription = _firestoreService
|
||||||
.getDeviceHistoriesStream(deviceId.value)
|
.getDeviceHistoriesStream(deviceId.value)
|
||||||
|
|
@ -257,12 +325,16 @@ class HomePatientController extends GetxController {
|
||||||
|
|
||||||
void _processHistoryData(List<HistoryModel> histories) {
|
void _processHistoryData(List<HistoryModel> histories) {
|
||||||
try {
|
try {
|
||||||
|
// Ambil 10 data TERBARU
|
||||||
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
final latestHistories = histories.take(10).toList();
|
final latestHistories = histories.take(10).toList();
|
||||||
|
|
||||||
|
// Urutkan lagi dari lama -> baru supaya grafik terbaca kiri ke kanan
|
||||||
latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
|
||||||
final List<ChartDataPoint> points = latestHistories.map((history) {
|
final List<ChartDataPoint> points = latestHistories.map((history) {
|
||||||
return ChartDataPoint(
|
return ChartDataPoint(
|
||||||
hour: history.timestamp.hour,
|
timestamp: history.timestamp,
|
||||||
dropsPerMinute: history.dropPerMinute,
|
dropsPerMinute: history.dropPerMinute,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
@ -278,8 +350,7 @@ class HomePatientController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isLoadingNotifications.value = true;
|
isLoadingNotifications.value = true;
|
||||||
|
|
||||||
_notificationsSubscription
|
_notificationsSubscription?.cancel();
|
||||||
?.cancel(); // Cancel previous subscription if exists
|
|
||||||
|
|
||||||
_notificationsSubscription = _firestoreService
|
_notificationsSubscription = _firestoreService
|
||||||
.getNotificationsStream()
|
.getNotificationsStream()
|
||||||
|
|
@ -313,13 +384,12 @@ class HomePatientController extends GetxController {
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error processing notification ${notif.id}: $e');
|
print('Error processing notification ${notif.id}: $e');
|
||||||
// Continue with other notifications
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
notifList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
|
||||||
notificationsList.value = notifList;
|
notificationsList.value = notifList.take(2).toList();
|
||||||
isLoadingNotifications.value = false;
|
isLoadingNotifications.value = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error processing notifications: $e');
|
print('Error processing notifications: $e');
|
||||||
|
|
@ -349,6 +419,7 @@ class HomePatientController extends GetxController {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
|
|
@ -370,9 +441,7 @@ class HomePatientController extends GetxController {
|
||||||
await _openWhatsApp();
|
await _openWhatsApp();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(
|
backgroundColor: const Color(0xFF25D366),
|
||||||
0xFF25D366,
|
|
||||||
), // WhatsApp green color
|
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|
@ -395,8 +464,7 @@ class HomePatientController extends GetxController {
|
||||||
|
|
||||||
Future<void> _openWhatsApp() async {
|
Future<void> _openWhatsApp() async {
|
||||||
try {
|
try {
|
||||||
const String nursePhoneNumber =
|
const String nursePhoneNumber = '6281231901277';
|
||||||
'6281234567890';
|
|
||||||
|
|
||||||
final String message = Uri.encodeComponent(
|
final String message = Uri.encodeComponent(
|
||||||
'Halo, saya ${patientName.value} di ruangan ${currentInfusData.value.room}. '
|
'Halo, saya ${patientName.value} di ruangan ${currentInfusData.value.room}. '
|
||||||
|
|
@ -506,11 +574,19 @@ class HomePatientController extends GetxController {
|
||||||
Future<void> refreshData() async {
|
Future<void> refreshData() async {
|
||||||
try {
|
try {
|
||||||
await _loadPatientData();
|
await _loadPatientData();
|
||||||
// Tidak perlu reload history karena sudah menggunakan stream
|
|
||||||
// History akan auto-update
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error refreshing data: $e');
|
print('Error refreshing data: $e');
|
||||||
AppSnackbar.error('Gagal refresh data: $e');
|
AppSnackbar.error('Gagal refresh data: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void clearDeviceAlert(String deviceId) {
|
||||||
|
_alertedDevices.remove(deviceId);
|
||||||
|
_notificationService.clearCooldown(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAllAlerts() {
|
||||||
|
_alertedDevices.clear();
|
||||||
|
_notificationService.clearAllCooldowns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,6 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(HomePatientController());
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
@ -33,9 +31,9 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => Text(
|
() => Text(
|
||||||
'Halo ${controller.patientName.value}',
|
'Halo Wali Pasien ${controller.patientName.value}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|
@ -67,36 +65,111 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
|
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// ── BUZZER ALERT BANNER ──────────────────────────────────
|
||||||
|
Obx(() {
|
||||||
|
if (!controller.isBuzzerActive.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[50],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.red[200]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[100],
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.notifications_active_rounded,
|
||||||
|
color: Colors.red[700],
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Buzzer Aktif!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.red[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
'Tetesan infus berhenti. Harap hubungi perawat.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.red[600],
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.turnOffBuzzer,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red[700],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.volume_off_rounded, size: 18),
|
||||||
|
SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'Matikan',
|
||||||
|
style: TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// ── END BUZZER ALERT BANNER ──────────────────────────────
|
||||||
|
|
||||||
// Monitoring Infus Section
|
// Monitoring Infus Section
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: const [
|
||||||
const Text(
|
Text(
|
||||||
'Monitoring Infus',
|
'Monitoring Infus',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Navigate to monitoring detail
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'See all',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Monitoring Card - Gunakan MonitoringCard dari widgets
|
// Monitoring Card
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
|
|
@ -114,24 +187,20 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
|
|
||||||
final infusData = controller.currentInfusData.value;
|
final infusData = controller.currentInfusData.value;
|
||||||
|
|
||||||
// Konversi ke InfusMonitoring untuk MonitoringCard
|
|
||||||
final monitoring = InfusMonitoring(
|
final monitoring = InfusMonitoring(
|
||||||
id: controller.deviceId.value,
|
id: controller.deviceId.value,
|
||||||
patientName: controller.patientName.value,
|
patientName: controller.patientName.value,
|
||||||
deviceId: infusData.deviceId,
|
deviceId: infusData.deviceId,
|
||||||
room: infusData.room,
|
room: infusData.room,
|
||||||
roomId: '', // Not needed for display
|
roomId: '',
|
||||||
dropsPerMinute: infusData.dropsPerMinute,
|
dropsPerMinute: infusData.dropsPerMinute,
|
||||||
lastUpdate:
|
lastUpdate: DateTime.now(),
|
||||||
DateTime.now(), // Will be calculated from updateTime
|
|
||||||
deviceStatus: infusData.deviceStatus,
|
deviceStatus: infusData.deviceStatus,
|
||||||
);
|
);
|
||||||
|
|
||||||
return MonitoringCard(
|
return MonitoringCard(
|
||||||
monitoring: monitoring,
|
monitoring: monitoring,
|
||||||
onTap: () {
|
onTap: () {},
|
||||||
// TODO: Navigate to detail
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -143,31 +212,19 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: const [
|
||||||
const Text(
|
Text(
|
||||||
'History Data',
|
'History Data',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Navigate to history detail
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'See all',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Chart Card - Gunakan HistoryGrafikCard dari widgets
|
// Chart Card
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
|
|
@ -185,9 +242,7 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
|
|
||||||
return HistoryGrafikCard(
|
return HistoryGrafikCard(
|
||||||
dataPoints: controller.chartDataPoints.toList(),
|
dataPoints: controller.chartDataPoints.toList(),
|
||||||
onBookmarkTap: () {
|
onBookmarkTap: () {},
|
||||||
// TODO: Bookmark functionality
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -199,26 +254,14 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: const [
|
||||||
const Text(
|
Text(
|
||||||
'History Notifikasi',
|
'History Notifikasi',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Navigate to notifications
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'See all',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xFF2196F3),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -278,9 +321,7 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
room: notification.room,
|
room: notification.room,
|
||||||
deviceId: notification.deviceId,
|
deviceId: notification.deviceId,
|
||||||
timeAgo: notification.timeAgo,
|
timeAgo: notification.timeAgo,
|
||||||
onTap: () {
|
onTap: () {},
|
||||||
// TODO: Handle notification tap
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -293,7 +334,6 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Floating Action Button - Call Nurse
|
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => controller.showCallDialog(context),
|
onPressed: () => controller.showCallDialog(context),
|
||||||
backgroundColor: const Color(0xFF2196F3),
|
backgroundColor: const Color(0xFF2196F3),
|
||||||
|
|
@ -301,4 +341,4 @@ class HomePatientView extends GetView<HomePatientController> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../../../services/firestore_service.dart';
|
import '../../../services/firestore_service.dart';
|
||||||
import '../../../services/realtime_database_service.dart';
|
import '../../../services/realtime_database_service.dart';
|
||||||
|
import '../../../services/notification_service.dart';
|
||||||
import '../../../models/patient_model.dart';
|
import '../../../models/patient_model.dart';
|
||||||
import '../../../models/room_model.dart';
|
import '../../../models/room_model.dart';
|
||||||
import '../../../models/realtime_monitoring_model.dart';
|
import '../../../models/realtime_monitoring_model.dart';
|
||||||
|
|
@ -35,6 +36,7 @@ class HomeController extends GetxController {
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
final FirestoreService _firestoreService = FirestoreService();
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService();
|
||||||
|
final NotificationService _notificationService = NotificationService();
|
||||||
|
|
||||||
final currentCarouselIndex = 0.obs;
|
final currentCarouselIndex = 0.obs;
|
||||||
final bannerImages = [
|
final bannerImages = [
|
||||||
|
|
@ -45,12 +47,27 @@ class HomeController extends GetxController {
|
||||||
final monitoringList = <InfusMonitoring>[].obs;
|
final monitoringList = <InfusMonitoring>[].obs;
|
||||||
final isLoading = true.obs;
|
final isLoading = true.obs;
|
||||||
|
|
||||||
|
List<RoomModel> _cachedRooms = [];
|
||||||
|
|
||||||
StreamSubscription? _patientsSubscription;
|
StreamSubscription? _patientsSubscription;
|
||||||
StreamSubscription? _realtimeSubscription;
|
StreamSubscription? _roomsSubscription;
|
||||||
|
|
||||||
|
// Map deviceId -> StreamSubscription untuk per-device realtime stream
|
||||||
|
final Map<String, StreamSubscription> _deviceSubscriptions = {};
|
||||||
|
|
||||||
|
// Map deviceId -> data realtime terbaru
|
||||||
|
final Map<String, RealtimeMonitoringModel> _realtimeDataMap = {};
|
||||||
|
|
||||||
|
List<PatientModel> _latestPatients = [];
|
||||||
|
|
||||||
|
bool _patientsReady = false;
|
||||||
|
|
||||||
|
final Set<String> _alertedDevices = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
_initializeNotificationService();
|
||||||
_initializeMonitoring();
|
_initializeMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,44 +75,128 @@ class HomeController extends GetxController {
|
||||||
void onClose() {
|
void onClose() {
|
||||||
searchController.dispose();
|
searchController.dispose();
|
||||||
_patientsSubscription?.cancel();
|
_patientsSubscription?.cancel();
|
||||||
_realtimeSubscription?.cancel();
|
_roomsSubscription?.cancel();
|
||||||
|
// Cancel semua per-device subscriptions
|
||||||
|
for (final sub in _deviceSubscriptions.values) {
|
||||||
|
sub.cancel();
|
||||||
|
}
|
||||||
|
_deviceSubscriptions.clear();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeNotificationService() async {
|
||||||
|
try {
|
||||||
|
await _notificationService.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error initializing notification service: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _initializeMonitoring() {
|
void _initializeMonitoring() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
_patientsReady = false;
|
||||||
|
|
||||||
|
// 1. Cache rooms
|
||||||
|
_roomsSubscription = _firestoreService.getRoomsStream().listen((rooms) {
|
||||||
|
_cachedRooms = rooms;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Patients stream
|
||||||
_patientsSubscription = _firestoreService.getPatientsStream().listen(
|
_patientsSubscription = _firestoreService.getPatientsStream().listen(
|
||||||
(patients) async {
|
(patients) {
|
||||||
_realtimeSubscription = _realtimeService.getAllDevicesStream().listen((
|
_latestPatients = patients;
|
||||||
realtimeDevices,
|
_patientsReady = true;
|
||||||
) {
|
_syncDeviceSubscriptions(patients);
|
||||||
_combineMonitoringData(patients, realtimeDevices);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
print('Error patients stream: $error');
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
AppSnackbar.error('Gagal memuat data');
|
AppSnackbar.error('Gagal memuat data pasien');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Timeout fallback
|
||||||
|
Future.delayed(const Duration(seconds: 8), () {
|
||||||
|
if (isLoading.value) {
|
||||||
|
print('Timeout: force stop loading');
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _combineMonitoringData(
|
/// Sinkronisasi per-device stream — ikuti pola HomePatientController
|
||||||
List<PatientModel> patients,
|
/// pakai getDeviceStream(dbRoomId, deviceId) bukan getAllDevicesStream()
|
||||||
List<RealtimeMonitoringModel> realtimeDevices,
|
void _syncDeviceSubscriptions(List<PatientModel> patients) {
|
||||||
) async {
|
final currentDeviceIds = patients.map((p) => p.deviceId).toSet();
|
||||||
|
final existingDeviceIds = _deviceSubscriptions.keys.toSet();
|
||||||
|
|
||||||
|
// Cancel stream untuk device yang sudah tidak ada di patients
|
||||||
|
final removedIds = existingDeviceIds.difference(currentDeviceIds);
|
||||||
|
for (final deviceId in removedIds) {
|
||||||
|
_deviceSubscriptions[deviceId]?.cancel();
|
||||||
|
_deviceSubscriptions.remove(deviceId);
|
||||||
|
_realtimeDataMap.remove(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambah stream untuk device baru
|
||||||
|
final newIds = currentDeviceIds.difference(existingDeviceIds);
|
||||||
|
for (final patient in patients.where((p) => newIds.contains(p.deviceId))) {
|
||||||
|
_subscribeToDevice(patient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subscribeToDevice(PatientModel patient) {
|
||||||
|
// Format room ID sama persis seperti HomePatientController
|
||||||
|
final dbRoomId = patient.roomId.toLowerCase().replaceAll(' ', '_');
|
||||||
|
final deviceId = patient.deviceId;
|
||||||
|
|
||||||
|
print('Subscribing to device: $deviceId in room: $dbRoomId');
|
||||||
|
|
||||||
|
final sub = _realtimeService
|
||||||
|
.getDeviceStream(dbRoomId, deviceId)
|
||||||
|
.listen(
|
||||||
|
(device) {
|
||||||
|
if (device != null) {
|
||||||
|
_realtimeDataMap[deviceId] = device;
|
||||||
|
} else {
|
||||||
|
// Device null = disconnected, hapus dari map
|
||||||
|
_realtimeDataMap.remove(deviceId);
|
||||||
|
}
|
||||||
|
// Rebuild setiap ada update dari device manapun
|
||||||
|
if (_patientsReady) {
|
||||||
|
_rebuildMonitoringList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
print('Error stream device $deviceId: $error');
|
||||||
|
// Tetap rebuild walau error, device akan tampil sebagai disconnected
|
||||||
|
if (_patientsReady) {
|
||||||
|
_rebuildMonitoringList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_deviceSubscriptions[deviceId] = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _rebuildMonitoringList() {
|
||||||
final List<InfusMonitoring> combinedData = [];
|
final List<InfusMonitoring> combinedData = [];
|
||||||
|
|
||||||
for (var patient in patients) {
|
for (var patient in _latestPatients) {
|
||||||
final realtimeDevice = realtimeDevices.firstWhereOrNull(
|
final realtimeDevice = _realtimeDataMap[patient.deviceId];
|
||||||
(device) => device.deviceId == patient.deviceId,
|
final roomName = _getRoomNameFromCache(patient.roomId);
|
||||||
);
|
|
||||||
|
|
||||||
final roomName = await _getRoomName(patient.roomId);
|
|
||||||
final dropRate = realtimeDevice?.dropRate ?? 0.0;
|
final dropRate = realtimeDevice?.dropRate ?? 0.0;
|
||||||
final lastUpdate = realtimeDevice?.lastUpdate ?? DateTime.now();
|
final lastUpdate = realtimeDevice?.lastUpdate ?? DateTime.now();
|
||||||
final deviceStatus = realtimeDevice?.deviceStatus ?? 'disconnected';
|
final deviceStatus = realtimeDevice?.deviceStatus ?? 'disconnected';
|
||||||
|
|
||||||
|
_checkDropRate(
|
||||||
|
patient: patient,
|
||||||
|
dropRate: dropRate,
|
||||||
|
roomName: roomName,
|
||||||
|
deviceStatus: deviceStatus,
|
||||||
|
);
|
||||||
|
|
||||||
combinedData.add(
|
combinedData.add(
|
||||||
InfusMonitoring(
|
InfusMonitoring(
|
||||||
id: patient.id,
|
id: patient.id,
|
||||||
|
|
@ -114,16 +215,37 @@ class HomeController extends GetxController {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getRoomName(String roomId) async {
|
String _getRoomNameFromCache(String roomId) {
|
||||||
try {
|
final room = _cachedRooms.firstWhereOrNull(
|
||||||
final rooms = await _firestoreService.getRoomsStream().first;
|
(r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
|
||||||
final room = rooms.firstWhereOrNull(
|
);
|
||||||
(r) =>
|
return room?.roomName ?? roomId;
|
||||||
r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
|
}
|
||||||
);
|
|
||||||
return room?.roomName ?? roomId;
|
Future<void> _checkDropRate({
|
||||||
} catch (e) {
|
required PatientModel patient,
|
||||||
return roomId;
|
required double dropRate,
|
||||||
|
required String roomName,
|
||||||
|
required String deviceStatus,
|
||||||
|
}) async {
|
||||||
|
final deviceId = patient.deviceId;
|
||||||
|
|
||||||
|
if (dropRate == 0.0 && deviceStatus == 'connected') {
|
||||||
|
if (!_alertedDevices.contains(deviceId)) {
|
||||||
|
await _notificationService.showDropRateAlert(
|
||||||
|
patientId: patient.id,
|
||||||
|
patientName: patient.namePatient,
|
||||||
|
deviceId: deviceId,
|
||||||
|
roomName: roomName,
|
||||||
|
);
|
||||||
|
_alertedDevices.add(deviceId);
|
||||||
|
}
|
||||||
|
} else if (dropRate > 0.0) {
|
||||||
|
if (_alertedDevices.contains(deviceId)) {
|
||||||
|
_alertedDevices.remove(deviceId);
|
||||||
|
_notificationService.clearCooldown(deviceId);
|
||||||
|
await _notificationService.cancelNotificationByDeviceId(deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +329,9 @@ class HomeController extends GetxController {
|
||||||
|
|
||||||
String getLastUpdateText(DateTime lastUpdate) {
|
String getLastUpdateText(DateTime lastUpdate) {
|
||||||
final difference = DateTime.now().difference(lastUpdate);
|
final difference = DateTime.now().difference(lastUpdate);
|
||||||
if (difference.inMinutes < 60) {
|
if (difference.inMinutes < 1) {
|
||||||
|
return 'Just now';
|
||||||
|
} else if (difference.inMinutes < 60) {
|
||||||
return '${difference.inMinutes}m ago';
|
return '${difference.inMinutes}m ago';
|
||||||
} else if (difference.inHours < 24) {
|
} else if (difference.inHours < 24) {
|
||||||
return '${difference.inHours}h ago';
|
return '${difference.inHours}h ago';
|
||||||
|
|
@ -215,4 +339,14 @@ class HomeController extends GetxController {
|
||||||
return '${difference.inDays}d ago';
|
return '${difference.inDays}d ago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearDeviceAlert(String deviceId) {
|
||||||
|
_alertedDevices.remove(deviceId);
|
||||||
|
_notificationService.clearCooldown(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAllAlerts() {
|
||||||
|
_alertedDevices.clear();
|
||||||
|
_notificationService.clearAllCooldowns();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ class HomeView extends GetView<HomeController> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(HomeController());
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: const Color(0xFFF5F7FA),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -23,8 +21,9 @@ class HomeView extends GetView<HomeController> {
|
||||||
currentCarouselIndex: controller.currentCarouselIndex,
|
currentCarouselIndex: controller.currentCarouselIndex,
|
||||||
onPageChanged: controller.updateCarouselIndex,
|
onPageChanged: controller.updateCarouselIndex,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 16),
|
||||||
_buildSectionHeader(),
|
_buildSectionHeader(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Expanded(child: _buildMonitoringList()),
|
Expanded(child: _buildMonitoringList()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -33,24 +32,49 @@ class HomeView extends GetView<HomeController> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context) {
|
Widget _buildHeader(BuildContext context) {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Column(
|
||||||
'Halo Perawat',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
children: [
|
||||||
|
Text(
|
||||||
|
'Selamat Datang 👋',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
const Text(
|
||||||
|
'Halo, Perawat',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1A1D2E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => controller.showLogoutDialog(context),
|
onTap: () => controller.showLogoutDialog(context),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
color: const Color(0xFFFFF0F0),
|
||||||
borderRadius: BorderRadius.circular(10),
|
border: Border.all(color: const Color(0xFFFFCDD2)),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.logout_rounded,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.logout, color: Colors.red),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -66,15 +90,13 @@ class HomeView extends GetView<HomeController> {
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Monitoring Infus',
|
'Monitoring Infus',
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
),
|
fontSize: 17,
|
||||||
TextButton(
|
fontWeight: FontWeight.bold,
|
||||||
onPressed: () => Get.toNamed('/all-monitoring'),
|
color: Color(0xFF1A1D2E),
|
||||||
child: const Text(
|
|
||||||
'Lihat Semua',
|
|
||||||
style: TextStyle(color: Color(0xFF0091EA)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -83,52 +105,203 @@ class HomeView extends GetView<HomeController> {
|
||||||
Widget _buildMonitoringList() {
|
Widget _buildMonitoringList() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(
|
return _buildLoadingState();
|
||||||
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.monitoringList.isEmpty) {
|
if (controller.monitoringList.isEmpty) {
|
||||||
return _buildEmptyState();
|
return _buildEmptyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return RefreshIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
color: const Color(0xFF0091EA),
|
||||||
itemCount: controller.monitoringList.length,
|
onRefresh: () async {
|
||||||
itemBuilder: (context, index) {
|
// Trigger refresh — stream akan otomatis update,
|
||||||
final item = controller.monitoringList[index];
|
// ini hanya untuk UX pull-to-refresh
|
||||||
return MonitoringCard(
|
await Future.delayed(const Duration(milliseconds: 800));
|
||||||
monitoring: item,
|
|
||||||
onTap: () => Get.toNamed(
|
|
||||||
'/device-detail',
|
|
||||||
arguments: {
|
|
||||||
'deviceId': item.deviceId,
|
|
||||||
'roomId': item.roomId,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemCount: controller.monitoringList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = controller.monitoringList[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: MonitoringCard(
|
||||||
|
monitoring: item,
|
||||||
|
onTap: () => Get.toNamed(
|
||||||
|
'/device-detail',
|
||||||
|
arguments: {'deviceId': item.deviceId, 'roomId': item.roomId},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildLoadingState() {
|
||||||
return Center(
|
return ListView.builder(
|
||||||
child: Column(
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
itemCount: 4,
|
||||||
children: [
|
itemBuilder: (context, index) {
|
||||||
Icon(
|
return Padding(
|
||||||
Icons.medical_services_outlined,
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
size: 80,
|
child: _buildSkeletonCard(),
|
||||||
color: Colors.grey[400],
|
);
|
||||||
),
|
},
|
||||||
const SizedBox(height: 16),
|
);
|
||||||
Text(
|
}
|
||||||
'Tidak ada data monitoring',
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
Widget _buildSkeletonCard() {
|
||||||
|
return Container(
|
||||||
|
height: 110,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_shimmerBox(width: 36, height: 36, radius: 10),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_shimmerBox(width: 140, height: 13, radius: 6),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_shimmerBox(width: 90, height: 11, radius: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
_shimmerBox(width: 60, height: 26, radius: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_shimmerBox(width: 80, height: 11, radius: 6),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_shimmerBox(width: 60, height: 11, radius: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Widget _shimmerBox({
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
double radius = 4,
|
||||||
|
}) {
|
||||||
|
return _ShimmerWidget(
|
||||||
|
child: Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE3F2FD),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.water_drop_outlined,
|
||||||
|
size: 48,
|
||||||
|
color: Color(0xFF0091EA),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Belum Ada Data',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1A1D2E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Data monitoring infus akan tampil di sini setelah perangkat terhubung.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget shimmer animasi untuk skeleton loading
|
||||||
|
class _ShimmerWidget extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
const _ShimmerWidget({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ShimmerWidget> createState() => _ShimmerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShimmerWidgetState extends State<_ShimmerWidget>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
_animation = Tween<double>(
|
||||||
|
begin: 0.4,
|
||||||
|
end: 1.0,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(opacity: _animation, child: widget.child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ class LoginController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
void login() async {
|
void login() async {
|
||||||
// Validasi input
|
|
||||||
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
|
|
@ -30,7 +29,6 @@ class LoginController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi format email
|
|
||||||
if (!GetUtils.isEmail(usernameController.text)) {
|
if (!GetUtils.isEmail(usernameController.text)) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
|
|
@ -45,27 +43,23 @@ class LoginController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Login dengan Firebase Authentication
|
|
||||||
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
|
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
|
||||||
email: usernameController.text.trim(),
|
email: usernameController.text.trim(),
|
||||||
password: passwordController.text.trim(),
|
password: passwordController.text.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userCredential.user != null) {
|
if (userCredential.user != null) {
|
||||||
// Cek apakah user ada di collection users
|
|
||||||
final userDoc = await _firestore
|
final userDoc = await _firestore
|
||||||
.collection('users')
|
.collection('users')
|
||||||
.doc(userCredential.user!.uid)
|
.doc(userCredential.user!.uid)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (userDoc.exists) {
|
if (userDoc.exists) {
|
||||||
// User ada di collection users (Guardian/Patient)
|
|
||||||
final userData = userDoc.data()!;
|
final userData = userDoc.data()!;
|
||||||
final deviceId = userData['device_id'] as String?;
|
final deviceId = userData['device_id'] as String?;
|
||||||
final userName = userData['name'] as String? ?? 'User';
|
final userName = userData['name'] as String? ?? 'User';
|
||||||
|
|
||||||
if (deviceId != null && deviceId.isNotEmpty) {
|
if (deviceId != null && deviceId.isNotEmpty) {
|
||||||
// User adalah guardian/patient dengan device_id
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Berhasil',
|
'Berhasil',
|
||||||
'Selamat datang, $userName!',
|
'Selamat datang, $userName!',
|
||||||
|
|
@ -74,7 +68,6 @@ class LoginController extends GetxController {
|
||||||
snackPosition: SnackPosition.TOP,
|
snackPosition: SnackPosition.TOP,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigasi ke home_patient dengan device_id
|
|
||||||
Get.offAllNamed(
|
Get.offAllNamed(
|
||||||
'/home-patient',
|
'/home-patient',
|
||||||
arguments: {
|
arguments: {
|
||||||
|
|
@ -84,7 +77,6 @@ class LoginController extends GetxController {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// User ada tapi tidak punya device_id
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Akun Anda belum terdaftar dengan device',
|
'Akun Anda belum terdaftar dengan device',
|
||||||
|
|
@ -95,7 +87,6 @@ class LoginController extends GetxController {
|
||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User TIDAK ada di collection users = Perawat/Admin
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Berhasil',
|
'Berhasil',
|
||||||
'Login berhasil! Selamat datang, Perawat',
|
'Login berhasil! Selamat datang, Perawat',
|
||||||
|
|
@ -104,7 +95,6 @@ class LoginController extends GetxController {
|
||||||
snackPosition: SnackPosition.TOP,
|
snackPosition: SnackPosition.TOP,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigasi ke navbar/home perawat
|
|
||||||
Get.offAllNamed('/navbar');
|
Get.offAllNamed('/navbar');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,12 +146,20 @@ class LoginController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
void contactNurse() async {
|
void contactNurse() async {
|
||||||
const phoneNumber = '+6281231901277'; // Tanpa spasi
|
const phoneNumber = '6281231901277';
|
||||||
final url = Uri.parse('https://wa.me/$phoneNumber');
|
final message = Uri.encodeComponent(
|
||||||
|
'Halo, saya wali pasien dari (Nama Pasien) '
|
||||||
|
'Saya ingin meminta akun untuk login ke aplikasi Smart Infus.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construct the WhatsApp URL with the message
|
||||||
|
final url = Uri.parse('https://wa.me/$phoneNumber?text=$message');
|
||||||
|
|
||||||
|
// Attempt to launch WhatsApp
|
||||||
if (await canLaunchUrl(url)) {
|
if (await canLaunchUrl(url)) {
|
||||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||||
} else {
|
} else {
|
||||||
|
// Show an error if WhatsApp can't be opened
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Tidak dapat membuka WhatsApp',
|
'Tidak dapat membuka WhatsApp',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:get/get.dart';
|
||||||
import '../controllers/navbar_controller.dart';
|
import '../controllers/navbar_controller.dart';
|
||||||
import '../../home/controllers/home_controller.dart';
|
import '../../home/controllers/home_controller.dart';
|
||||||
import '../../notification/controllers/notification_controller.dart';
|
import '../../notification/controllers/notification_controller.dart';
|
||||||
import '../../schedule/controllers/schedule_controller.dart';
|
// import '../../schedule/controllers/schedule_controller.dart';
|
||||||
import '../../profile/controllers/profile_controller.dart';
|
import '../../profile/controllers/profile_controller.dart';
|
||||||
|
|
||||||
class NavbarBinding extends Bindings {
|
class NavbarBinding extends Bindings {
|
||||||
|
|
@ -11,7 +11,7 @@ class NavbarBinding extends Bindings {
|
||||||
Get.lazyPut<NavbarController>(() => NavbarController());
|
Get.lazyPut<NavbarController>(() => NavbarController());
|
||||||
Get.lazyPut<HomeController>(() => HomeController());
|
Get.lazyPut<HomeController>(() => HomeController());
|
||||||
Get.lazyPut<NotificationController>(() => NotificationController());
|
Get.lazyPut<NotificationController>(() => NotificationController());
|
||||||
Get.lazyPut<ScheduleController>(() => ScheduleController());
|
// Get.lazyPut<ScheduleController>(() => ScheduleController());
|
||||||
Get.lazyPut<ProfileController>(() => ProfileController());
|
Get.lazyPut<ProfileController>(() => ProfileController());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||||
import '../controllers/navbar_controller.dart';
|
import '../controllers/navbar_controller.dart';
|
||||||
import '../../home/views/home_view.dart';
|
import '../../home/views/home_view.dart';
|
||||||
import '../../notification/views/notification_view.dart';
|
import '../../notification/views/notification_view.dart';
|
||||||
import '../../schedule/views/schedule_view.dart';
|
// import '../../schedule/views/schedule_view.dart';
|
||||||
import '../../profile/views/profile_view.dart';
|
import '../../profile/views/profile_view.dart';
|
||||||
|
|
||||||
class NavbarView extends StatelessWidget {
|
class NavbarView extends StatelessWidget {
|
||||||
|
|
@ -16,7 +16,7 @@ class NavbarView extends StatelessWidget {
|
||||||
final pages = const [
|
final pages = const [
|
||||||
HomeView(),
|
HomeView(),
|
||||||
NotificationView(),
|
NotificationView(),
|
||||||
ScheduleView(),
|
// ScheduleView(),
|
||||||
ProfileView(),
|
ProfileView(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -52,8 +52,8 @@ class NavbarView extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
_navItem(controller, Icons.home, 0, 'Home'),
|
_navItem(controller, Icons.home, 0, 'Home'),
|
||||||
_navItem(controller, Icons.chat_bubble_outline, 1, 'Notification'),
|
_navItem(controller, Icons.chat_bubble_outline, 1, 'Notification'),
|
||||||
_navItem(controller, Icons.calendar_today, 2, 'Schedule'),
|
// _navItem(controller, Icons.calendar_today, 2, 'Schedule'),
|
||||||
_navItem(controller, Icons.person_outline, 3, 'Account'),
|
_navItem(controller, Icons.person_outline, 2, 'Account'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ class NotificationItem {
|
||||||
|
|
||||||
class NotificationController extends GetxController {
|
class NotificationController extends GetxController {
|
||||||
final FirestoreService _firestoreService = FirestoreService();
|
final FirestoreService _firestoreService = FirestoreService();
|
||||||
|
|
||||||
final selectedTab = 0.obs;
|
final selectedTab = 0.obs;
|
||||||
final _allNotifications = <NotificationItem>[].obs;
|
final _allNotifications = <NotificationItem>[].obs;
|
||||||
final isLoading = true.obs;
|
final isLoading = true.obs;
|
||||||
|
|
@ -76,47 +76,51 @@ class NotificationController extends GetxController {
|
||||||
void _initializeNotifications() {
|
void _initializeNotifications() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
_notificationsSubscription = _firestoreService
|
_notificationsSubscription = _firestoreService.getNotificationsStream().listen(
|
||||||
.getNotificationsStream()
|
(notificationModels) async {
|
||||||
.listen((notificationModels) async {
|
final List<NotificationItem> items = [];
|
||||||
final List<NotificationItem> items = [];
|
|
||||||
|
|
||||||
for (var notifModel in notificationModels) {
|
for (var notifModel in notificationModels) {
|
||||||
// Get patient info
|
// Get patient info
|
||||||
final patient = await _firestoreService.getPatientById(
|
final patient = await _firestoreService.getPatientById(
|
||||||
notifModel.patientId,
|
notifModel.patientId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get room name from patient's roomId
|
// Get room name from patient's roomId
|
||||||
String roomName = 'Unknown Room';
|
String roomName = 'Unknown Room';
|
||||||
if (patient != null && patient.roomId.isNotEmpty) {
|
if (patient != null && patient.roomId.isNotEmpty) {
|
||||||
final rooms = await _firestoreService.getRoomsStream().first;
|
final rooms = await _firestoreService.getRoomsStream().first;
|
||||||
final room = rooms.firstWhereOrNull(
|
final room = rooms.firstWhereOrNull(
|
||||||
(r) => r.roomName.toLowerCase() == patient.roomId.toLowerCase(),
|
(r) =>
|
||||||
|
r.id ==
|
||||||
|
patient
|
||||||
|
.roomId, // ✅ UBAH INI - cari berdasarkan ID, bukan roomName
|
||||||
|
);
|
||||||
|
roomName = room?.roomName ?? patient.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
NotificationItem(
|
||||||
|
id: notifModel.id,
|
||||||
|
title: notifModel.title,
|
||||||
|
message: notifModel.message,
|
||||||
|
room: roomName,
|
||||||
|
deviceId: notifModel.deviceId,
|
||||||
|
patientName: patient?.namePatient ?? 'Unknown Patient',
|
||||||
|
time: notifModel.createdAt,
|
||||||
|
isRead: notifModel.isRead,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
roomName = room?.roomName ?? patient.roomId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(
|
_allNotifications.value = items;
|
||||||
NotificationItem(
|
isLoading.value = false;
|
||||||
id: notifModel.id,
|
},
|
||||||
title: notifModel.title,
|
onError: (error) {
|
||||||
message: notifModel.message,
|
print('Error listening to notifications: $error');
|
||||||
room: roomName,
|
isLoading.value = false;
|
||||||
deviceId: notifModel.deviceId,
|
},
|
||||||
patientName: patient?.namePatient ?? 'Unknown Patient',
|
);
|
||||||
time: notifModel.createdAt,
|
|
||||||
isRead: notifModel.isRead,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_allNotifications.value = items;
|
|
||||||
isLoading.value = false;
|
|
||||||
}, onError: (error) {
|
|
||||||
print('Error listening to notifications: $error');
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed property untuk notifikasi yang difilter
|
// Computed property untuk notifikasi yang difilter
|
||||||
|
|
@ -205,4 +209,4 @@ class NotificationController extends GetxController {
|
||||||
print('Error adding notification: $e');
|
print('Error adding notification: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,15 +87,6 @@ class NotificationView extends GetView<NotificationController> {
|
||||||
notification: notif,
|
notification: notif,
|
||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
controller.deleteNotification(notif.id);
|
controller.deleteNotification(notif.id);
|
||||||
Get.snackbar(
|
|
||||||
'Notifikasi Dihapus',
|
|
||||||
'Notifikasi telah dihapus',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
backgroundColor: Colors.grey[800],
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onTap: !notif.isRead ? () {
|
onTap: !notif.isRead ? () {
|
||||||
controller.markAsRead(notif.id);
|
controller.markAsRead(notif.id);
|
||||||
|
|
@ -105,15 +96,6 @@ class NotificationView extends GetView<NotificationController> {
|
||||||
controller.changeTab(1);
|
controller.changeTab(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
Get.snackbar(
|
|
||||||
'Notifikasi Dibaca',
|
|
||||||
'Notifikasi telah dipindahkan ke sudah dibaca',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
backgroundColor: const Color(0xFF0091EA),
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
} : null,
|
} : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ class ProfileController extends GetxController {
|
||||||
},
|
},
|
||||||
'monitoring': {
|
'monitoring': {
|
||||||
'device_status': 'connected',
|
'device_status': 'connected',
|
||||||
'drop_rate': 0,
|
'drop_rate': 1,
|
||||||
'last_update': DateTime.now().toIso8601String(),
|
'last_update': DateTime.now().toIso8601String(),
|
||||||
},
|
},
|
||||||
'settings': {'servo_angle_open': 90, 'servo_angle_close': 0},
|
'settings': {'servo_angle_open': 90, 'servo_angle_close': 0},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import '../../../models/schedule_model.dart';
|
||||||
import '../../../widgets/app_snackbar.dart';
|
import '../../../widgets/app_snackbar.dart';
|
||||||
|
|
||||||
class PatientSchedule {
|
class PatientSchedule {
|
||||||
final String id;
|
final String id;
|
||||||
|
final String patientId;
|
||||||
final String name;
|
final String name;
|
||||||
final String room;
|
final String room;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
|
|
@ -15,6 +16,7 @@ class PatientSchedule {
|
||||||
|
|
||||||
PatientSchedule({
|
PatientSchedule({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.patientId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
|
|
@ -61,45 +63,48 @@ class ScheduleController extends GetxController {
|
||||||
_schedulesSubscription = _firestoreService
|
_schedulesSubscription = _firestoreService
|
||||||
.getSchedulesStream(date: selectedDate.value)
|
.getSchedulesStream(date: selectedDate.value)
|
||||||
.listen(
|
.listen(
|
||||||
(scheduleModels) async {
|
(scheduleModels) async {
|
||||||
try {
|
|
||||||
final List<PatientSchedule> items = [];
|
|
||||||
|
|
||||||
for (var scheduleModel in scheduleModels) {
|
|
||||||
try {
|
try {
|
||||||
final patient = await _firestoreService.getPatientById(scheduleModel.patientId);
|
final List<PatientSchedule> items = [];
|
||||||
|
|
||||||
if (patient != null) {
|
for (var scheduleModel in scheduleModels) {
|
||||||
final roomName = await _getRoomName(patient.roomId);
|
try {
|
||||||
items.add(
|
final patient = await _firestoreService.getPatientById(
|
||||||
PatientSchedule(
|
scheduleModel.patientId,
|
||||||
id: scheduleModel.id,
|
);
|
||||||
name: patient.namePatient,
|
|
||||||
room: roomName,
|
if (patient != null) {
|
||||||
deviceId: patient.deviceId,
|
final roomName = await _getRoomName(patient.roomId);
|
||||||
medicineTime: scheduleModel.medicineDetail,
|
items.add(
|
||||||
fluidTime: scheduleModel.fluidDetail,
|
PatientSchedule(
|
||||||
timeOfDay: scheduleModel.timeOfDay,
|
id: scheduleModel.id,
|
||||||
),
|
patientId: scheduleModel.patientId,
|
||||||
);
|
name: patient.namePatient,
|
||||||
|
room: roomName,
|
||||||
|
deviceId: patient.deviceId,
|
||||||
|
medicineTime: scheduleModel.medicineDetail,
|
||||||
|
fluidTime: scheduleModel.fluidDetail,
|
||||||
|
timeOfDay: scheduleModel.timeOfDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allSchedules.value = items;
|
allSchedules.value = items;
|
||||||
_applyTimeFilter();
|
_applyTimeFilter();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
AppSnackbar.error('Gagal memuat jadwal');
|
AppSnackbar.error('Gagal memuat jadwal');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyTimeFilter() {
|
void _applyTimeFilter() {
|
||||||
|
|
@ -113,7 +118,8 @@ class ScheduleController extends GetxController {
|
||||||
try {
|
try {
|
||||||
final rooms = await _firestoreService.getRoomsStream().first;
|
final rooms = await _firestoreService.getRoomsStream().first;
|
||||||
final room = rooms.firstWhereOrNull(
|
final room = rooms.firstWhereOrNull(
|
||||||
(r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
|
(r) =>
|
||||||
|
r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(),
|
||||||
);
|
);
|
||||||
return room?.roomName ?? roomId;
|
return room?.roomName ?? roomId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -135,16 +141,14 @@ class ScheduleController extends GetxController {
|
||||||
_loadSchedules();
|
_loadSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addSchedule(PatientSchedule schedule, DateTime scheduleDate) async {
|
Future<void> addSchedule(
|
||||||
|
PatientSchedule schedule,
|
||||||
|
DateTime scheduleDate,
|
||||||
|
) async {
|
||||||
try {
|
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(
|
final scheduleModel = ScheduleModel(
|
||||||
id: '',
|
id: '',
|
||||||
patientId: patient.id,
|
patientId: schedule.patientId,
|
||||||
medicineDetail: schedule.medicineTime,
|
medicineDetail: schedule.medicineTime,
|
||||||
fluidDetail: schedule.fluidTime,
|
fluidDetail: schedule.fluidTime,
|
||||||
scheduleDate: scheduleDate,
|
scheduleDate: scheduleDate,
|
||||||
|
|
@ -155,20 +159,19 @@ class ScheduleController extends GetxController {
|
||||||
await _firestoreService.addSchedule(scheduleModel);
|
await _firestoreService.addSchedule(scheduleModel);
|
||||||
AppSnackbar.success('Jadwal berhasil ditambahkan');
|
AppSnackbar.success('Jadwal berhasil ditambahkan');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppSnackbar.error('Gagal menambahkan jadwal');
|
AppSnackbar.error('Gagal menambahkan jadwal: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateSchedule(String id, PatientSchedule updatedSchedule, DateTime scheduleDate) async {
|
Future<void> updateSchedule(
|
||||||
|
String scheduleId,
|
||||||
|
PatientSchedule updatedSchedule,
|
||||||
|
DateTime scheduleDate,
|
||||||
|
) async {
|
||||||
try {
|
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(
|
final scheduleModel = ScheduleModel(
|
||||||
id: id,
|
id: scheduleId,
|
||||||
patientId: patient.id,
|
patientId: updatedSchedule.patientId,
|
||||||
medicineDetail: updatedSchedule.medicineTime,
|
medicineDetail: updatedSchedule.medicineTime,
|
||||||
fluidDetail: updatedSchedule.fluidTime,
|
fluidDetail: updatedSchedule.fluidTime,
|
||||||
scheduleDate: scheduleDate,
|
scheduleDate: scheduleDate,
|
||||||
|
|
@ -176,10 +179,10 @@ class ScheduleController extends GetxController {
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _firestoreService.updateSchedule(id, scheduleModel);
|
await _firestoreService.updateSchedule(scheduleId, scheduleModel);
|
||||||
AppSnackbar.success('Jadwal berhasil diperbarui');
|
AppSnackbar.success('Jadwal berhasil diperbarui');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppSnackbar.error('Gagal memperbarui jadwal');
|
AppSnackbar.error('Gagal memperbarui jadwal: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +191,7 @@ class ScheduleController extends GetxController {
|
||||||
await _firestoreService.deleteSchedule(id);
|
await _firestoreService.deleteSchedule(id);
|
||||||
AppSnackbar.success('Jadwal berhasil dihapus');
|
AppSnackbar.success('Jadwal berhasil dihapus');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppSnackbar.error('Gagal menghapus jadwal');
|
AppSnackbar.error('Gagal menghapus jadwal: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,9 +199,13 @@ class ScheduleController extends GetxController {
|
||||||
try {
|
try {
|
||||||
final patients = await _firestoreService.getPatientsStream().first;
|
final patients = await _firestoreService.getPatientsStream().first;
|
||||||
final List<Map<String, String>> result = [];
|
final List<Map<String, String>> result = [];
|
||||||
|
final Set<String> seenIds = {};
|
||||||
|
|
||||||
for (var patient in patients) {
|
for (var patient in patients) {
|
||||||
try {
|
try {
|
||||||
|
// Skip jika ID sudah pernah ditambahkan (mencegah duplikasi)
|
||||||
|
if (seenIds.contains(patient.id)) continue;
|
||||||
|
|
||||||
final roomName = await _getRoomName(patient.roomId);
|
final roomName = await _getRoomName(patient.roomId);
|
||||||
result.add({
|
result.add({
|
||||||
'id': patient.id,
|
'id': patient.id,
|
||||||
|
|
@ -206,6 +213,8 @@ class ScheduleController extends GetxController {
|
||||||
'room': roomName,
|
'room': roomName,
|
||||||
'device': patient.deviceId,
|
'device': patient.deviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
seenIds.add(patient.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -216,4 +225,4 @@ class ScheduleController extends GetxController {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
|
|
||||||
class SplashScreenController extends GetxController {
|
class SplashScreenController extends GetxController {
|
||||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
|
@ -13,16 +15,44 @@ class SplashScreenController extends GetxController {
|
||||||
|
|
||||||
void _checkAuthStatus() async {
|
void _checkAuthStatus() async {
|
||||||
await Future.delayed(const Duration(seconds: 4));
|
await Future.delayed(const Duration(seconds: 4));
|
||||||
|
|
||||||
// Cek apakah user sudah login
|
|
||||||
User? user = _auth.currentUser;
|
User? user = _auth.currentUser;
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
// User sudah login, langsung ke home/navbar
|
try {
|
||||||
Get.offAllNamed(Routes.NAVBAR);
|
final userDoc = await _firestore
|
||||||
|
.collection('users')
|
||||||
|
.doc(user.uid)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (userDoc.exists) {
|
||||||
|
final userData = userDoc.data()!;
|
||||||
|
final deviceId = userData['device_id'] as String?;
|
||||||
|
final userName = userData['name'] as String? ?? 'User';
|
||||||
|
|
||||||
|
if (deviceId != null && deviceId.isNotEmpty) {
|
||||||
|
Get.offAllNamed(
|
||||||
|
Routes.HOME_PATIENT,
|
||||||
|
arguments: {
|
||||||
|
'deviceId': deviceId,
|
||||||
|
'userName': userName,
|
||||||
|
'userEmail': user.email ?? '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _auth.signOut();
|
||||||
|
Get.offAllNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Get.offAllNamed(Routes.NAVBAR);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking user data: $e');
|
||||||
|
await _auth.signOut();
|
||||||
|
Get.offAllNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// User belum login, ke halaman login
|
|
||||||
Get.offAllNamed(Routes.LOGIN);
|
Get.offAllNamed(Routes.LOGIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class SplashScreenView extends GetView<SplashScreenController> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
const Text(
|
const Text(
|
||||||
'Smart Infuse',
|
'SMART INFUSE',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
// lib/app/services/notification_service.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:awesome_notifications/awesome_notifications.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import '../models/notification_model.dart' as local;
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
|
factory NotificationService() => _instance;
|
||||||
|
NotificationService._internal();
|
||||||
|
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final Map<String, DateTime> _lastNotificationTime = {};
|
||||||
|
final Duration _notificationCooldown = const Duration(minutes: 5);
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
await AwesomeNotifications().initialize(null, [
|
||||||
|
NotificationChannel(
|
||||||
|
channelKey: 'infus_alert_channel',
|
||||||
|
channelName: 'Infus Alerts',
|
||||||
|
channelDescription: 'Alerts for infusion monitoring',
|
||||||
|
defaultColor: const Color(0xFF9D50DD),
|
||||||
|
ledColor: Colors.white,
|
||||||
|
importance: NotificationImportance.High,
|
||||||
|
channelShowBadge: true,
|
||||||
|
playSound: true,
|
||||||
|
enableVibration: true,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await _requestPermissions();
|
||||||
|
_setupNotificationListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestPermissions() async {
|
||||||
|
await AwesomeNotifications().isNotificationAllowed().then((
|
||||||
|
isAllowed,
|
||||||
|
) async {
|
||||||
|
if (!isAllowed) {
|
||||||
|
await AwesomeNotifications().requestPermissionToSendNotifications();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupNotificationListeners() {
|
||||||
|
AwesomeNotifications().setListeners(
|
||||||
|
onActionReceivedMethod: _onNotificationTapped,
|
||||||
|
onNotificationCreatedMethod: _onNotificationCreated,
|
||||||
|
onNotificationDisplayedMethod: _onNotificationDisplayed,
|
||||||
|
onDismissActionReceivedMethod: _onNotificationDismissed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma("vm:entry-point")
|
||||||
|
static Future<void> _onNotificationCreated(
|
||||||
|
ReceivedNotification receivedNotification,
|
||||||
|
) async {
|
||||||
|
print('Notification created: ${receivedNotification.id}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma("vm:entry-point")
|
||||||
|
static Future<void> _onNotificationDisplayed(
|
||||||
|
ReceivedNotification receivedNotification,
|
||||||
|
) async {
|
||||||
|
print('Notification displayed: ${receivedNotification.id}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma("vm:entry-point")
|
||||||
|
static Future<void> _onNotificationTapped(
|
||||||
|
ReceivedAction receivedAction,
|
||||||
|
) async {
|
||||||
|
print('Notification tapped: ${receivedAction.payload}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma("vm:entry-point")
|
||||||
|
static Future<void> _onNotificationDismissed(
|
||||||
|
ReceivedAction receivedAction,
|
||||||
|
) async {
|
||||||
|
print('Notification dismissed: ${receivedAction.id}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showDropRateAlert({
|
||||||
|
required String patientId,
|
||||||
|
required String patientName,
|
||||||
|
required String deviceId,
|
||||||
|
required String roomName,
|
||||||
|
}) async {
|
||||||
|
if (_isInCooldown(deviceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final title = 'Peringatan Infus ⚠️';
|
||||||
|
final message =
|
||||||
|
'Infus $patientName di ruang $roomName telah berhenti menetes! Segera lakukan Tindakan!';
|
||||||
|
|
||||||
|
await AwesomeNotifications().createNotification(
|
||||||
|
content: NotificationContent(
|
||||||
|
id: deviceId.hashCode,
|
||||||
|
channelKey: 'infus_alert_channel',
|
||||||
|
title: title,
|
||||||
|
body: message,
|
||||||
|
notificationLayout: NotificationLayout.Default,
|
||||||
|
payload: {
|
||||||
|
'device_id': deviceId,
|
||||||
|
'patient_id': patientId,
|
||||||
|
'patient_name': patientName,
|
||||||
|
'room_name': roomName,
|
||||||
|
},
|
||||||
|
category: NotificationCategory.Alarm,
|
||||||
|
wakeUpScreen: true,
|
||||||
|
fullScreenIntent: true,
|
||||||
|
autoDismissible: false,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
actionButtons: [
|
||||||
|
NotificationActionButton(
|
||||||
|
key: 'VIEW',
|
||||||
|
label: 'Lihat Detail',
|
||||||
|
actionType: ActionType.Default,
|
||||||
|
),
|
||||||
|
NotificationActionButton(
|
||||||
|
key: 'DISMISS',
|
||||||
|
label: 'Tutup',
|
||||||
|
actionType: ActionType.DismissAction,
|
||||||
|
isDangerousOption: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await _saveNotificationToFirestore(
|
||||||
|
patientId: patientId,
|
||||||
|
deviceId: deviceId,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
|
||||||
|
_lastNotificationTime[deviceId] = DateTime.now();
|
||||||
|
print('Notification sent for device: $deviceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showDropRateAlertForPatient({
|
||||||
|
required String patientId,
|
||||||
|
required String patientName,
|
||||||
|
required String deviceId,
|
||||||
|
required String roomName,
|
||||||
|
}) async {
|
||||||
|
if (_isInCooldown(deviceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final title = 'Peringatan Infus ⚠️';
|
||||||
|
final message =
|
||||||
|
'Infus $patientName di ruang $roomName telah berhenti menetes! Segera hubungi Perawat!';
|
||||||
|
|
||||||
|
await AwesomeNotifications().createNotification(
|
||||||
|
content: NotificationContent(
|
||||||
|
id: deviceId.hashCode,
|
||||||
|
channelKey: 'infus_alert_channel',
|
||||||
|
title: title,
|
||||||
|
body: message,
|
||||||
|
notificationLayout: NotificationLayout.Default,
|
||||||
|
payload: {
|
||||||
|
'device_id': deviceId,
|
||||||
|
'patient_id': patientId,
|
||||||
|
'patient_name': patientName,
|
||||||
|
'room_name': roomName,
|
||||||
|
},
|
||||||
|
category: NotificationCategory.Alarm,
|
||||||
|
wakeUpScreen: true,
|
||||||
|
fullScreenIntent: true,
|
||||||
|
autoDismissible: false,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
actionButtons: [
|
||||||
|
NotificationActionButton(
|
||||||
|
key: 'CALL',
|
||||||
|
label: 'Hubungi Perawat',
|
||||||
|
actionType: ActionType.Default,
|
||||||
|
),
|
||||||
|
NotificationActionButton(
|
||||||
|
key: 'DISMISS',
|
||||||
|
label: 'Tutup',
|
||||||
|
actionType: ActionType.DismissAction,
|
||||||
|
isDangerousOption: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await _saveNotificationToFirestore(
|
||||||
|
patientId: patientId,
|
||||||
|
deviceId: deviceId,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
|
||||||
|
_lastNotificationTime[deviceId] = DateTime.now();
|
||||||
|
print('Notification sent for patient device: $deviceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveNotificationToFirestore({
|
||||||
|
required String patientId,
|
||||||
|
required String deviceId,
|
||||||
|
required String title,
|
||||||
|
required String message,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final notification = local.NotificationModel(
|
||||||
|
id: '',
|
||||||
|
patientId: patientId,
|
||||||
|
deviceId: deviceId,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
isRead: false,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _firestore
|
||||||
|
.collection('notifications')
|
||||||
|
.add(notification.toFirestore());
|
||||||
|
print('Notification saved to Firestore');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saving notification to Firestore: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isInCooldown(String deviceId) {
|
||||||
|
if (!_lastNotificationTime.containsKey(deviceId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastTime = _lastNotificationTime[deviceId]!;
|
||||||
|
final difference = DateTime.now().difference(lastTime);
|
||||||
|
return difference < _notificationCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCooldown(String deviceId) {
|
||||||
|
_lastNotificationTime.remove(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAllCooldowns() {
|
||||||
|
_lastNotificationTime.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelNotification(int id) async {
|
||||||
|
await AwesomeNotifications().cancel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelNotificationByDeviceId(String deviceId) async {
|
||||||
|
await AwesomeNotifications().cancel(deviceId.hashCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelAllNotifications() async {
|
||||||
|
await AwesomeNotifications().cancelAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_lastNotificationTime.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,10 +9,9 @@ class RealtimeDatabaseService {
|
||||||
Stream<Map<String, RealtimeMonitoringModel>> getRoomDevicesStream(
|
Stream<Map<String, RealtimeMonitoringModel>> getRoomDevicesStream(
|
||||||
String roomId,
|
String roomId,
|
||||||
) {
|
) {
|
||||||
return _database
|
return _database.child('realtime/rooms/$roomId/devices').onValue.map((
|
||||||
.child('realtime/rooms/$roomId/devices')
|
event,
|
||||||
.onValue
|
) {
|
||||||
.map((event) {
|
|
||||||
final Map<String, RealtimeMonitoringModel> devices = {};
|
final Map<String, RealtimeMonitoringModel> devices = {};
|
||||||
|
|
||||||
if (event.snapshot.value != null) {
|
if (event.snapshot.value != null) {
|
||||||
|
|
@ -40,12 +39,16 @@ class RealtimeDatabaseService {
|
||||||
.child('realtime/rooms/$roomId/devices/$deviceId')
|
.child('realtime/rooms/$roomId/devices/$deviceId')
|
||||||
.onValue
|
.onValue
|
||||||
.map((event) {
|
.map((event) {
|
||||||
if (event.snapshot.value != null) {
|
if (event.snapshot.value != null) {
|
||||||
final data = event.snapshot.value as Map<dynamic, dynamic>;
|
final data = event.snapshot.value as Map<dynamic, dynamic>;
|
||||||
return RealtimeMonitoringModel.fromRealtimeDB(deviceId, roomId, data);
|
return RealtimeMonitoringModel.fromRealtimeDB(
|
||||||
}
|
deviceId,
|
||||||
return null;
|
roomId,
|
||||||
});
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<RealtimeMonitoringModel>> getAllDevicesStream() {
|
Stream<List<RealtimeMonitoringModel>> getAllDevicesStream() {
|
||||||
|
|
@ -83,8 +86,9 @@ class RealtimeDatabaseService {
|
||||||
String deviceId,
|
String deviceId,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final snapshot =
|
final snapshot = await _database
|
||||||
await _database.child('realtime/rooms/$roomId/devices/$deviceId').get();
|
.child('realtime/rooms/$roomId/devices/$deviceId')
|
||||||
|
.get();
|
||||||
|
|
||||||
if (snapshot.value != null) {
|
if (snapshot.value != null) {
|
||||||
final data = snapshot.value as Map<dynamic, dynamic>;
|
final data = snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
|
@ -102,9 +106,9 @@ class RealtimeDatabaseService {
|
||||||
await _database
|
await _database
|
||||||
.child('realtime/rooms/$roomId/devices/$deviceId/controlling')
|
.child('realtime/rooms/$roomId/devices/$deviceId/controlling')
|
||||||
.update({
|
.update({
|
||||||
'servo_open': open,
|
'servo_open': open,
|
||||||
'last_command': DateTime.now().toIso8601String(),
|
'last_command': DateTime.now().toIso8601String(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDropRate(
|
Future<void> updateDropRate(
|
||||||
|
|
@ -115,9 +119,9 @@ class RealtimeDatabaseService {
|
||||||
await _database
|
await _database
|
||||||
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
||||||
.update({
|
.update({
|
||||||
'drop_rate': dropRate,
|
'drop_rate': dropRate,
|
||||||
'last_update': DateTime.now().toIso8601String(),
|
'last_update': DateTime.now().toIso8601String(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDeviceStatus(
|
Future<void> updateDeviceStatus(
|
||||||
|
|
@ -128,9 +132,9 @@ class RealtimeDatabaseService {
|
||||||
await _database
|
await _database
|
||||||
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
.child('realtime/rooms/$roomId/devices/$deviceId/monitoring')
|
||||||
.update({
|
.update({
|
||||||
'device_status': status,
|
'device_status': status,
|
||||||
'last_update': DateTime.now().toIso8601String(),
|
'last_update': DateTime.now().toIso8601String(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateServoSettings(
|
Future<void> updateServoSettings(
|
||||||
|
|
@ -142,9 +146,9 @@ class RealtimeDatabaseService {
|
||||||
await _database
|
await _database
|
||||||
.child('realtime/rooms/$roomId/devices/$deviceId/settings')
|
.child('realtime/rooms/$roomId/devices/$deviceId/settings')
|
||||||
.update({
|
.update({
|
||||||
'servo_angle_open': angleOpen,
|
'servo_angle_open': angleOpen,
|
||||||
'servo_angle_close': angleClose,
|
'servo_angle_close': angleClose,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UTILITY ====================
|
// ==================== UTILITY ====================
|
||||||
|
|
@ -158,11 +162,25 @@ class RealtimeDatabaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> controlBuzzer(
|
||||||
|
String roomId,
|
||||||
|
String deviceId,
|
||||||
|
bool active,
|
||||||
|
) async {
|
||||||
|
await _database
|
||||||
|
.child('realtime/rooms/$roomId/devices/$deviceId/controlling')
|
||||||
|
.update({
|
||||||
|
'buzzer_active': active,
|
||||||
|
'last_command': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<double> getDeviceDropRate(String roomId, String deviceId) async {
|
Future<double> getDeviceDropRate(String roomId, String deviceId) async {
|
||||||
try {
|
try {
|
||||||
final snapshot = await _database
|
final snapshot = await _database
|
||||||
.child(
|
.child(
|
||||||
'realtime/rooms/$roomId/devices/$deviceId/monitoring/drop_rate')
|
'realtime/rooms/$roomId/devices/$deviceId/monitoring/drop_rate',
|
||||||
|
)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (snapshot.value != null) {
|
if (snapshot.value != null) {
|
||||||
|
|
@ -173,4 +191,4 @@ class RealtimeDatabaseService {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,10 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final hours = dataPoints.map((p) => p.hour.toDouble()).toList();
|
|
||||||
final drops = dataPoints.map((p) => p.dropsPerMinute.toDouble()).toList();
|
final drops = dataPoints.map((p) => p.dropsPerMinute.toDouble()).toList();
|
||||||
|
|
||||||
final minX = hours.reduce((a, b) => a < b ? a : b);
|
final minX = 0.0;
|
||||||
final maxX = hours.reduce((a, b) => a > b ? a : b);
|
final maxX = (dataPoints.length - 1).toDouble();
|
||||||
final minY = (drops.reduce((a, b) => a < b ? a : b) - 5.0).clamp(
|
final minY = (drops.reduce((a, b) => a < b ? a : b) - 5.0).clamp(
|
||||||
0.0,
|
0.0,
|
||||||
double.infinity,
|
double.infinity,
|
||||||
|
|
@ -113,10 +112,14 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
reservedSize: 30,
|
reservedSize: 30,
|
||||||
interval: dataPoints.length > 5 ? 2.0 : 1.0,
|
interval: dataPoints.length > 5 ? 2.0 : 1.0,
|
||||||
getTitlesWidget: (double value, TitleMeta meta) {
|
getTitlesWidget: (double value, TitleMeta meta) {
|
||||||
|
final index = value.toInt();
|
||||||
|
if (index < 0 || index >= dataPoints.length) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${value.toInt()}:00',
|
dataPoints[index].timeLabel,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
@ -158,14 +161,12 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
maxY: maxY,
|
maxY: maxY,
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
spots: dataPoints
|
spots: List.generate(dataPoints.length, (index) {
|
||||||
.map(
|
return FlSpot(
|
||||||
(point) => FlSpot(
|
index.toDouble(),
|
||||||
point.hour.toDouble(),
|
dataPoints[index].dropsPerMinute.toDouble(),
|
||||||
point.dropsPerMinute.toDouble(),
|
);
|
||||||
),
|
}),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
isCurved: true,
|
isCurved: true,
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Color(0xFF2196F3), Color(0xFF1976D2)],
|
colors: [Color(0xFF2196F3), Color(0xFF1976D2)],
|
||||||
|
|
@ -208,8 +209,12 @@ class HistoryGrafikCard extends StatelessWidget {
|
||||||
tooltipMargin: 8,
|
tooltipMargin: 8,
|
||||||
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
|
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
|
||||||
return touchedBarSpots.map((barSpot) {
|
return touchedBarSpots.map((barSpot) {
|
||||||
|
final index = barSpot.x.toInt();
|
||||||
|
final label = (index >= 0 && index < dataPoints.length)
|
||||||
|
? dataPoints[index].timeLabel
|
||||||
|
: '';
|
||||||
return LineTooltipItem(
|
return LineTooltipItem(
|
||||||
'${barSpot.y.toInt()} tpm\n${barSpot.x.toInt()}:00',
|
'${barSpot.y.toInt()} tpm\n$label',
|
||||||
const TextStyle(
|
const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,17 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_medicineController = TextEditingController(text: widget.schedule?.medicineTime ?? '');
|
_medicineController = TextEditingController(
|
||||||
_fluidController = TextEditingController(text: widget.schedule?.fluidTime ?? '');
|
text: widget.schedule?.medicineTime ?? '',
|
||||||
|
);
|
||||||
|
_fluidController = TextEditingController(
|
||||||
|
text: widget.schedule?.fluidTime ?? '',
|
||||||
|
);
|
||||||
_selectedDate = DateTime.now();
|
_selectedDate = DateTime.now();
|
||||||
|
|
||||||
if (widget.isEdit && widget.schedule != null) {
|
if (widget.isEdit && widget.schedule != null) {
|
||||||
_selectedPatientId = widget.schedule!.id;
|
|
||||||
_selectedTime = widget.schedule!.timeOfDay;
|
_selectedTime = widget.schedule!.timeOfDay;
|
||||||
|
// _selectedPatientId akan diset setelah _loadPatients() selesai
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadPatients();
|
_loadPatients();
|
||||||
|
|
@ -46,11 +50,16 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
try {
|
try {
|
||||||
final controller = Get.find<ScheduleController>();
|
final controller = Get.find<ScheduleController>();
|
||||||
final patientsList = await controller.getPatientsList();
|
final patientsList = await controller.getPatientsList();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_patients = patientsList;
|
_patients = patientsList;
|
||||||
_isLoadingPatients = false;
|
_isLoadingPatients = false;
|
||||||
|
|
||||||
|
// Set selected patient ID untuk mode edit
|
||||||
|
if (widget.isEdit && widget.schedule != null) {
|
||||||
|
_selectedPatientId = widget.schedule!.patientId;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -101,7 +110,9 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(40),
|
padding: EdgeInsets.all(40),
|
||||||
child: CircularProgressIndicator(color: Color(0xFF0091EA)),
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFF0091EA),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
|
|
@ -140,7 +151,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildLabel('Detail Cairan & Tetes', Icons.water_drop),
|
_buildLabel(
|
||||||
|
'Detail Cairan & Tetes',
|
||||||
|
Icons.water_drop,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
controller: _fluidController,
|
controller: _fluidController,
|
||||||
|
|
@ -168,7 +182,9 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(colors: [Color(0xFF0091EA), Color(0xFF0277BD)]),
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF0091EA), Color(0xFF0277BD)],
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(24),
|
topLeft: Radius.circular(24),
|
||||||
topRight: Radius.circular(24),
|
topRight: Radius.circular(24),
|
||||||
|
|
@ -182,7 +198,11 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.white.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -258,16 +278,25 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.calendar_today, size: 18, color: Color(0xFF0091EA)),
|
const Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xFF0091EA),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
_selectedDate != null
|
_selectedDate != null
|
||||||
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(_selectedDate!)
|
? DateFormat(
|
||||||
|
'EEEE, dd MMMM yyyy',
|
||||||
|
'id_ID',
|
||||||
|
).format(_selectedDate!)
|
||||||
: 'Pilih tanggal...',
|
: 'Pilih tanggal...',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: _selectedDate != null ? const Color(0xFF424242) : Colors.grey[400],
|
color: _selectedDate != null
|
||||||
|
? const Color(0xFF424242)
|
||||||
|
: Colors.grey[400],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
@ -296,6 +325,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
|
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
value: _selectedPatientId,
|
value: _selectedPatientId,
|
||||||
|
dropdownColor: Colors.white,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cari dan pilih pasien...',
|
hintText: 'Cari dan pilih pasien...',
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
|
|
@ -317,7 +347,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent),
|
borderSide: const BorderSide(color: Colors.redAccent),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
|
@ -334,11 +367,13 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: widget.isEdit
|
||||||
setState(() {
|
? null
|
||||||
_selectedPatientId = value;
|
: (value) {
|
||||||
});
|
setState(() {
|
||||||
},
|
_selectedPatientId = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 'Pilih pasien terlebih dahulu';
|
return 'Pilih pasien terlebih dahulu';
|
||||||
|
|
@ -388,7 +423,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFE3F2FD),
|
color: const Color(0xFFE3F2FD),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
|
@ -404,7 +442,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
color: Colors.grey[100],
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
|
@ -445,6 +486,7 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
Widget _buildTimeDropdown() {
|
Widget _buildTimeDropdown() {
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
value: _selectedTime,
|
value: _selectedTime,
|
||||||
|
dropdownColor: Colors.white,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Pilih waktu...',
|
hintText: 'Pilih waktu...',
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
|
|
@ -466,7 +508,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent),
|
borderSide: const BorderSide(color: Colors.redAccent),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
|
@ -561,7 +606,10 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
|
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -584,7 +632,9 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
side: BorderSide(color: Colors.grey.shade300, width: 2),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Batal',
|
'Batal',
|
||||||
|
|
@ -618,7 +668,9 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.isEdit ? 'Simpan' : 'Tambah',
|
widget.isEdit ? 'Simpan' : 'Tambah',
|
||||||
|
|
@ -649,19 +701,25 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final controller = Get.find<ScheduleController>();
|
final controller = Get.find<ScheduleController>();
|
||||||
|
final patientData = _selectedPatientData!;
|
||||||
|
|
||||||
final schedule = PatientSchedule(
|
final schedule = PatientSchedule(
|
||||||
id: _selectedPatientId!,
|
id: widget.isEdit ? widget.schedule!.id : '',
|
||||||
name: _selectedPatientData!['name']!,
|
patientId: _selectedPatientId!,
|
||||||
room: _selectedPatientData!['room']!,
|
name: patientData['name']!,
|
||||||
deviceId: _selectedPatientData!['device']!,
|
room: patientData['room']!,
|
||||||
|
deviceId: patientData['device']!,
|
||||||
medicineTime: _medicineController.text.trim(),
|
medicineTime: _medicineController.text.trim(),
|
||||||
fluidTime: _fluidController.text.trim(),
|
fluidTime: _fluidController.text.trim(),
|
||||||
timeOfDay: _selectedTime!,
|
timeOfDay: _selectedTime!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isEdit && widget.schedule != null) {
|
if (widget.isEdit && widget.schedule != null) {
|
||||||
controller.updateSchedule(widget.schedule!.id, schedule, _selectedDate!);
|
controller.updateSchedule(
|
||||||
|
widget.schedule!.id,
|
||||||
|
schedule,
|
||||||
|
_selectedDate!,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
controller.addSchedule(schedule, _selectedDate!);
|
controller.addSchedule(schedule, _selectedDate!);
|
||||||
}
|
}
|
||||||
|
|
@ -669,4 +727,4 @@ class _ModalScheduleAddEditState extends State<ModalScheduleAddEdit> {
|
||||||
Get.back();
|
Get.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ class MonitoringCard extends StatelessWidget {
|
||||||
final InfusMonitoring monitoring;
|
final InfusMonitoring monitoring;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const MonitoringCard({
|
const MonitoringCard({super.key, required this.monitoring, this.onTap});
|
||||||
super.key,
|
|
||||||
required this.monitoring,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -158,11 +154,7 @@ class MonitoringCard extends StatelessWidget {
|
||||||
const Color(0xFFE3F2FD),
|
const Color(0xFFE3F2FD),
|
||||||
const Color(0xFF2196F3),
|
const Color(0xFF2196F3),
|
||||||
),
|
),
|
||||||
_buildBadge(
|
_buildBadge(monitoring.room, Colors.grey[100]!, Colors.grey[700]!),
|
||||||
monitoring.room,
|
|
||||||
Colors.grey[100]!,
|
|
||||||
Colors.grey[700]!,
|
|
||||||
),
|
|
||||||
_buildBadge(
|
_buildBadge(
|
||||||
isConnected ? 'Online' : 'Offline',
|
isConnected ? 'Online' : 'Offline',
|
||||||
isConnected ? const Color(0xFFE8F5E9) : Colors.red[50]!,
|
isConnected ? const Color(0xFFE8F5E9) : Colors.red[50]!,
|
||||||
|
|
@ -196,4 +188,4 @@ class MonitoringCard extends StatelessWidget {
|
||||||
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||||
return '${diff.inDays}d ago';
|
return '${diff.inDays}d ago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <awesome_notifications/awesome_notifications_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) awesome_notifications_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "AwesomeNotificationsPlugin");
|
||||||
|
awesome_notifications_plugin_register_with_registrar(awesome_notifications_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
awesome_notifications
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import awesome_notifications
|
||||||
import cloud_firestore
|
import cloud_firestore
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
|
|
@ -13,6 +14,7 @@ import path_provider_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin"))
|
||||||
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
awesome_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: awesome_notifications
|
||||||
|
sha256: "0d5fa4457f2ba4e536adc3ef6af709cdcecf4a05a1f3035981e9afa2f899b2a8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ dependencies:
|
||||||
firebase_auth: ^6.1.4
|
firebase_auth: ^6.1.4
|
||||||
firebase_database: ^12.1.2
|
firebase_database: ^12.1.2
|
||||||
cloud_firestore: ^6.1.2
|
cloud_firestore: ^6.1.2
|
||||||
|
awesome_notifications: ^0.10.1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <awesome_notifications/awesome_notifications_plugin_c_api.h>
|
||||||
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AwesomeNotificationsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi"));
|
||||||
CloudFirestorePluginCApiRegisterWithRegistrar(
|
CloudFirestorePluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
|
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
|
||||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
awesome_notifications
|
||||||
cloud_firestore
|
cloud_firestore
|
||||||
firebase_auth
|
firebase_auth
|
||||||
firebase_core
|
firebase_core
|
||||||
|
|
|
||||||