CR Implementation contd.

CR-19328-LiveCare-Enhancements
haroon amjad 1 month ago
parent 08e2cc1f3a
commit 31ed9483ad

@ -2058,5 +2058,19 @@
"liveCareTermsHeading16": "14.\tالشكاوى",
"liveCareTermsConditions47": "خدمات الرعاية الصحية عن بُعد الخاصة بنا سوف تكون لتخصصات طبية محددة أو لمواعيد المتابعة أو إعادة صرف الدواء. ",
"liveCareTermsConditions48": "إذا كانت لديك أي شكاوى أو مخاوف بشأن التطبيق و/أو موقع الويب أو خدماتنا أو كيفية تعاملنا مع معلوماتك الشخصية، فيرجى التواصل معنا على: EServices.HMG@drsulaimanalhabib.com أو الاتصال على الرقم: 9553 525 011",
"liveCareInsurancePolicyText": "تنبيه: الاجراءات ما بعد الاستشارة (مثل التحاليل أو الأدوية) غير مشمولة بتغطية التأمين, ويتحمل المريض تكلفتها."
"liveCareInsurancePolicyText": "تنبيه: الاجراءات ما بعد الاستشارة (مثل التحاليل أو الأدوية) غير مشمولة بتغطية التأمين, ويتحمل المريض تكلفتها.",
"liveCarePendingRequestText": "أنت الآن في قائمة الانتظار. سيتواصل معك الطبيب في أقرب وقت ممكن, الرجاء التأكد من تفعيل اشعارات التطبيق وضبط اعدادات الكاميرا والمايكروفون بشكل صحيح.",
"testConnection": "اختبار الاتصال",
"cameraPermissionText": "إذن الكاميرا",
"microphonePermissionText": "إذن استخدام الميكروفون",
"notificationPermissionText": "إذن الإشعارات",
"testLiveCareConnection": "اختبار اتصال لايف كير",
"cameraFeedInactive": "بث الكاميرا غير نشط",
"cantWaitLiveCare": "معاودة الاتصال بي, لا أستطيع الانتظار”",
"whenCallBackLiveCare": "متى تود أن نعاود الاتصال بك؟",
"after30Mins": "اتصل بي بعد 30 دقيقة",
"after30MinsDesc": "سيتصل بك الطبيب بعد 30 دقيقة.",
"after1Hour": "اتصل بي بعد ساعة",
"after1HourDesc": "سيتصل بك الطبيب بعد ساعة واحدة."
}

@ -2048,7 +2048,21 @@
"liveCareTermsHeading16": "14. COMPLAINTS",
"liveCareTermsConditions47": "Our Telehealth Services will be for specific medical specialties or follow-up or medication refill appointments.",
"liveCareTermsConditions48": "If you have any complaints or concerns about the Application and or the Website, Our Services, or how we handle your personal information please contact us on: EServices.HMG@drsulaimanalhabib.com or call 011 525 9553",
"liveCareInsurancePolicyText": "Note: Procedure following the consultation (such as Lab tests or medication) are not covered by insurance and will be charged directly to the patient."
"liveCareInsurancePolicyText": "Note: Procedure following the consultation (such as Lab tests or medication) are not covered by insurance and will be charged directly to the patient.",
"liveCarePendingRequestText": "You are now in the waiting queue. The doctor will contact you as soon as possible. Please make sure your App Notifications are turned on, and your camera and microphone settings are properly configured.",
"testConnection": "Test Connection",
"cameraPermissionText": "Camera Permission",
"microphonePermissionText": "Microphone Permission",
"notificationPermissionText": "Notifications Permission",
"testLiveCareConnection": "Test LiveCare Connection",
"cameraFeedInactive": "Camera feed inactive",
"cantWaitLiveCare": "I can't wait, Please call me later",
"whenCallBackLiveCare": "When would you like to be called back?",
"after30Mins": "After 30 Mins",
"after30MinsDesc": "You will be called by the doctor after 30 mins",
"after1Hour": "After 1 Hour",
"after1HourDesc": "You will be called by the doctor after 1 hour"
}

@ -1023,6 +1023,8 @@ const DOWNLOAD_PHARMACY_INVOICE_PDF = 'Services/Notifications.svc/REST/DownloadI
const GET_APPOINTMENT_PARKING_QR = 'Services/outps.svc/rest/pms_getParkingTicketByAppointmentNo';
const RESCHEDULE_LIVECARE_CALL = 'Services/ER_VirtualCall.svc/ArrangeCall';
class ApiKeyConstants {
static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng';
}

@ -314,7 +314,7 @@ class ImmediateLiveCareViewModel extends ChangeNotifier {
} else if (apiResponse.messageStatus == 1) {
patientLiveCareHistoryList = apiResponse.data!;
if (patientLiveCareHistoryList.isNotEmpty) {
if (patientLiveCareHistoryList[0].callStatus! < 4) {
if ((patientLiveCareHistoryList[0].callStatus ?? 0) < 4 || (patientLiveCareHistoryList[0].callStatus ?? 0) == 15) {
patientHasPendingLiveCareRequest = true;
countdownDuration = Duration(minutes: patientLiveCareHistoryList.first.watingtimeInteger!, seconds: 0);
durationNotifier.value = countdownDuration;

@ -17,6 +17,7 @@ class PatientLiveCareHistory {
String? stringCallStatus;
int? vCID;
int? watingtimeInteger;
String? callType;
PatientLiveCareHistory(
{this.appointmentNo,
@ -36,7 +37,8 @@ class PatientLiveCareHistory {
this.serviceID,
this.stringCallStatus,
this.vCID,
this.watingtimeInteger});
this.watingtimeInteger,
this.callType});
PatientLiveCareHistory.fromJson(Map<String, dynamic> json) {
appointmentNo = json['AppointmentNo'];
@ -57,6 +59,7 @@ class PatientLiveCareHistory {
stringCallStatus = json['StringCallStatus'];
vCID = json['VC_ID'];
watingtimeInteger = json['WatingtimeInteger'];
callType = json['CallType'];
}
Map<String, dynamic> toJson() {

@ -2039,5 +2039,18 @@ abstract class LocaleKeys {
static const liveCareTermsConditions47 = 'liveCareTermsConditions47';
static const liveCareTermsConditions48 = 'liveCareTermsConditions48';
static const liveCareInsurancePolicyText = 'liveCareInsurancePolicyText';
static const liveCarePendingRequestText = 'liveCarePendingRequestText';
static const testConnection = 'testConnection';
static const cameraPermissionText = 'cameraPermissionText';
static const microphonePermissionText = 'microphonePermissionText';
static const notificationPermissionText = 'notificationPermissionText';
static const testLiveCareConnection = 'testLiveCareConnection';
static const cameraFeedInactive = 'cameraFeedInactive';
static const cantWaitLiveCare = 'cantWaitLiveCare';
static const whenCallBackLiveCare = 'whenCallBackLiveCare';
static const after30Mins = 'after30Mins';
static const after30MinsDesc = 'after30MinsDesc';
static const after1Hour = 'after1Hour';
static const after1HourDesc = 'after1HourDesc';
}

@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.da
import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/livecare_reschedule_widget.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/test_connection_sheet.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -47,7 +48,6 @@ class _ImmediateLiveCarePendingRequestPageState extends State<ImmediateLiveCareP
}
void _initializeVideoPlayer() {
debugPrint('🎥 Initializing video player...');
_videoController = VideoPlayerController.networkUrl(
Uri.parse('https://marathoon.com/service/hmgvideo/compressedvideo.mp4'),
)..initialize().then((_) {
@ -143,11 +143,32 @@ class _ImmediateLiveCarePendingRequestPageState extends State<ImmediateLiveCareP
),
SizedBox(height: 8.h),
SizedBox(height: 8.h),
LocaleKeys.yourTurnIsAfterPatients.tr(namedArgs: {
'count': (immediateLiveCareViewModel.patientLiveCareHistoryList.isNotEmpty ? immediateLiveCareViewModel.patientLiveCareHistoryList[0].patCount ?? 0 : 0).toString()
}, context: context).toText14(isBold: true),
// "Your turn is after ${immediateLiveCareViewModel.patientLiveCareHistoryList[0].patCount} patients.".toText16(isBold: true),
SizedBox(height: 8.h),
LocaleKeys.liveCarePendingRequestText.tr(context: context).toText14(isBold: true),
SizedBox(height: 16.h),
(immediateLiveCareViewModel.patientLiveCareHistoryList.first.callType ?? "").toLowerCase() == "video"
? CustomButton(
text: LocaleKeys.testConnection.tr(context: context),
onPressed: () {
showCommonBottomSheetWithoutHeight(context,
child: ZoomConnectionTestSheet(),
callBackFunc: () async {},
title: LocaleKeys.testLiveCareConnection.tr(context: context),
isCloseButtonVisible: true,
isFullScreen: false);
},
backgroundColor: AppColors.infoColor.withValues(alpha: 0.20),
borderColor: AppColors.infoColor.withValues(alpha: 0.01),
textColor: AppColors.infoColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconColor: AppColors.infoColor,
iconSize: 18.h,
icon: AppAssets.call,
)
: SizedBox.shrink(),
],
),
),
@ -180,10 +201,10 @@ class _ImmediateLiveCarePendingRequestPageState extends State<ImmediateLiveCareP
),
SizedBox(height: 16.h),
CustomButton(
text: "I can't wait, Please call me later",
text: LocaleKeys.cantWaitLiveCare.tr(context: context),
onPressed: () {
showCommonBottomSheetWithoutHeight(context,
child: LiveCareRescheduleWidget(), callBackFunc: () async {}, title: "When would you like to be called back?", isCloseButtonVisible: true, isFullScreen: false);
child: LiveCareRescheduleWidget(), callBackFunc: () async {}, title: LocaleKeys.whenCallBackLiveCare.tr(context: context), isCloseButtonVisible: true, isFullScreen: false);
},
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20),
borderColor: AppColors.warningColorYellow.withValues(alpha: 0.01),

@ -23,8 +23,8 @@ class LiveCareRescheduleWidget extends StatelessWidget {
children: [
checkInOptionCard(
AppAssets.liveCareRescheduleIcon,
"After 30 mins",
"You will be called by the doctor after 30 mins",
LocaleKeys.after30Mins.tr(context: context),
LocaleKeys.after30MinsDesc.tr(context: context),
// "Call for immediate assistance",
).onPress(() {
Navigator.of(context).pop();
@ -32,8 +32,8 @@ class LiveCareRescheduleWidget extends StatelessWidget {
SizedBox(height: 16.h),
checkInOptionCard(
AppAssets.liveCareRescheduleIcon,
"After 1 hour",
"You will be called by the doctor after 1 hour",
LocaleKeys.after1Hour.tr(context: context),
LocaleKeys.after1HourDesc.tr(context: context),
).onPress(() async {
Navigator.of(context).pop();
}),

@ -0,0 +1,177 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as zoom_view;
import '../../../../generated/locale_keys.g.dart';
class ZoomConnectionTestSheet extends StatefulWidget {
const ZoomConnectionTestSheet({super.key});
@override
State<ZoomConnectionTestSheet> createState() => _ZoomConnectionTestSheetState();
}
class _ZoomConnectionTestSheetState extends State<ZoomConnectionTestSheet> {
PermissionStatus? _cameraPermission;
PermissionStatus? _micPermission;
PermissionStatus? _notificationPermission;
final ZoomVideoSdk _zoomSdk = ZoomVideoSdk();
bool _isPreviewingCamera = false;
@override
void dispose() {
_stopCameraPreview();
super.dispose();
}
// 1. Request and verify system permissions
Future<void> _checkPermissions() async {
final cameraStatus = await Permission.camera.request();
final micStatus = await Permission.microphone.request();
final notificationStatus = await Permission.notification.request();
setState(() {
_cameraPermission = cameraStatus;
_micPermission = micStatus;
_notificationPermission = notificationStatus;
});
if (cameraStatus.isGranted && micStatus.isGranted) {
_startCameraPreview();
}
}
// 2. Turn on camera feed via Zoom Video SDK VideoHelper
Future<void> _startCameraPreview() async {
try {
await _zoomSdk.videoHelper.startVideo();
setState(() {
_isPreviewingCamera = true;
});
} catch (e) {
debugPrint('Error starting Zoom camera feed: $e');
}
}
Future<void> _stopCameraPreview() async {
if (_isPreviewingCamera) {
await _zoomSdk.videoHelper.stopVideo();
setState(() {
_isPreviewingCamera = false;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 'Zoom Device Diagnostics'.toText18(isBold: true),
// const SizedBox(height: 16),
// Status Indicators
_StatusRow(label: LocaleKeys.cameraPermissionText.tr(context: context), status: _cameraPermission),
_StatusRow(label: LocaleKeys.microphonePermissionText.tr(context: context), status: _micPermission),
_StatusRow(label: LocaleKeys.notificationPermissionText.tr(context: context), status: _notificationPermission),
SizedBox(height: 16.h),
// Zoom Native Platform View Widget
Container(
height: 200.h,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.antiAlias,
child: _isPreviewingCamera
? zoom_view.View(
creationParams: const {
"userId": "",
"sharing": false,
"preview": true, // Instructs SDK to operate in local preview mode
"focused": false,
"hasMultiCamera": false,
"multiCameraIndex": "0",
"videoAspect": "FullFilled",
"fullScreen": false,
},
)
: Center(
child: Text(
LocaleKeys.cameraFeedInactive.tr(),
style: TextStyle(color: Colors.white70),
),
),
),
SizedBox(height: 20.h),
CustomButton(
text: LocaleKeys.testConnection.tr(context: context),
onPressed: () {
_checkPermissions();
},
backgroundColor: AppColors.infoColor.withValues(alpha: 0.20),
borderColor: AppColors.infoColor.withValues(alpha: 0.01),
textColor: AppColors.infoColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconColor: AppColors.infoColor,
iconSize: 18.h,
icon: AppAssets.call,
)
// ElevatedButton.icon(
// icon: const Icon(Icons.sensors),
// label: const Text('Test Permissions & Devices'),
// onPressed: _checkPermissions,
// )
],
),
);
}
}
class _StatusRow extends StatelessWidget {
final String label;
final PermissionStatus? status;
const _StatusRow({required this.label, this.status});
@override
Widget build(BuildContext context) {
final isOk = status?.isGranted ?? false;
final icon = status == null
? Icons.help_outline
: isOk
? Icons.check_circle
: Icons.cancel;
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Icon(icon, color: status == null ? Colors.grey : (isOk ? Colors.green : Colors.red)),
],
),
);
}
}
Loading…
Cancel
Save