first commit

This commit is contained in:
MohammadFerryAlfansyah 2026-07-29 23:50:22 +07:00
parent 314bcc3b59
commit 3300d82c8d
52 changed files with 1334 additions and 414 deletions

View File

@ -1,3 +1,4 @@
{ {
"java.configuration.updateBuildConfiguration": "automatic" "java.configuration.updateBuildConfiguration": "automatic",
"cmake.sourceDirectory": "E:/JOKI/smartinfuse/linux"
} }

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

View File

@ -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)';
} }
} }

View File

@ -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,

View File

@ -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();
} }
} }

View File

@ -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> {
), ),
); );
} }
} }

View File

@ -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();
}
}

View File

@ -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> {
), ),
); );
} }
} }

View File

@ -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();
}
} }

View File

@ -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);
}
}

View File

@ -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',

View File

@ -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());
} }
} }

View File

@ -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'),
], ],
), ),
); );

View File

@ -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');
} }
} }
} }

View File

@ -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,
); );
}, },

View File

@ -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},

View File

@ -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 [];
} }
} }
} }

View File

@ -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);
} }
} }
} }

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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;
} }
} }
} }

View File

@ -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,

View File

@ -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();
} }
} }
} }

View File

@ -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';
} }
} }

View File

@ -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);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
awesome_notifications
url_launcher_linux url_launcher_linux
) )

View File

@ -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"))

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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