diff --git a/android/agconnect-services.json b/android/agconnect-services.json deleted file mode 100644 index 91aad2c..0000000 --- a/android/agconnect-services.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "agcgw":{ - "backurl":"connect-drcn.hispace.hicloud.com", - "url":"connect-drcn.dbankcloud.cn", - "websocketbackurl":"connect-ws-drcn.hispace.dbankcloud.com", - "websocketurl":"connect-ws-drcn.hispace.dbankcloud.cn" - }, - "agcgw_all":{ - "CN":"connect-drcn.dbankcloud.cn", - "CN_back":"connect-drcn.hispace.hicloud.com", - "DE":"connect-dre.dbankcloud.cn", - "DE_back":"connect-dre.hispace.hicloud.com", - "RU":"connect-drru.dbankcloud.cn", - "RU_back":"connect-drru.hispace.hicloud.com", - "SG":"connect-dra.dbankcloud.cn", - "SG_back":"connect-dra.hispace.hicloud.com" - }, - "client":{ - "cp_id":"2640966000002322881", - "product_id":"736430079244816567", - "client_id":"563735388191982656", - "client_secret":"650C7C799812AFFD53A10C7756CF05FB9F215A7E49032ABA8EBF3E14B77535CF", - "project_id":"736430079244816567", - "app_id":"102857389", - "api_key":"CgB6e3x9DJzMgRCmnT6dyUEkp6UsIfddb6l3w0ZEXzeiRMHEFi3400Z5fJ5qaHneU0OrAI/JRpk+DMGVs3QpUxlI", - "package_name":"com.ejada.hmg" - }, - "oauth_client":{ - "client_id":"102857389", - "client_type":1 - }, - "app_info":{ - "app_id":"102857389", - "package_name":"com.ejada.hmg" - }, - "service":{ - "analytics":{ - "collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", - "collector_url_ru":"datacollector-drru.dt.hicloud.com,datacollector-drru.dt.dbankcloud.cn", - "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", - "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", - "resource_id":"p1", - "channel_id":"" - }, - "search":{ - "url":"https://search-drcn.cloud.huawei.com" - }, - "cloudstorage":{ - "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn" - }, - "ml":{ - "mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn" - } - }, - "region":"CN", - "configuration_version":"3.0", - "appInfos":[ - { - "package_name":"com.ejada.hmg", - "client":{ - "app_id":"102857389" - }, - "app_info":{ - "package_name":"com.ejada.hmg", - "app_id":"102857389" - }, - "oauth_client":{ - "client_type":1, - "client_id":"102857389" - } - } - ] -} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 35dbe1b..2987d3b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.ejada.hmg" // minSdk = 24 minSdk = 26 - targetSdk = 35 - compileSdk = 35 + targetSdk = 36 + compileSdk = 36 // targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/lib/presentation/appointments/widgets/appointment_queueing_screen.dart b/android/app/keystores/keystore similarity index 100% rename from lib/presentation/appointments/widgets/appointment_queueing_screen.dart rename to android/app/keystores/keystore diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index ab7b2ca..9c1827a 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -69,4 +69,7 @@ # Penguin classes -keep class com.peng.pennavmap.models.** { *; } --keep class com.peng.pennavmap.db.** { *; } \ No newline at end of file +-keep class com.peng.pennavmap.db.** { *; } + +-keep class com.hiennv.flutter_callkit_incoming.** { *; } +-keepattributes Signature, Annotation, InnerClasses, EnclosingMethod \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 454a83c..8c1388b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,9 +30,6 @@ - @@ -98,6 +95,12 @@ + + + + + + @@ -122,6 +125,7 @@ - Push Notification Background @pragma('vm:entry-point') @@ -30,22 +31,89 @@ Future backgroundMessageHandler(dynamic message) async { // showCallkitIncoming(message); _incomingCall(message.data); return; - } else {} + } } callPage(String sessionID, String token) async {} _incomingCall(Map data) async { + print('the value of the _incomingCall remote message is $data'); + // LandingPage.incomingCallData = IncomingCallData.fromJson(data); // var dataItem = await AppSharedPreferences().getObject('call_data'); // if(dataItem != null ) return; // to stop repeated attempt to invoke the call // if (LandingPage.isOpenCallPage == false) { // LandingPage.isOpenCallPage = true; + + String roomID = data['session_id'] ?? ''; + + // GetIt.instance().saveString(key: CacheConst.zoomRoomID, value: roomID); + // GetIt.instance().saveBool(key: CacheConst.isAppOpenedFromCall, value: true); + + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, roomID); + Utils.saveBoolFromPrefs(CacheConst.isAppOpenedFromCall, true); + WidgetsFlutterBinding.ensureInitialized(); - // var _currentUuid = Uuid().v4(); - // await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); - // } - // LandingPage.isOpenCallPage = false; + + var _currentUuid = Uuid().v4(); + CallKitParams callKitParams = CallKitParams( + id: _currentUuid, + nameCaller: "Dr Al Habib", + appName: 'Dr Al Habib', + avatar: 'https://play-lh.googleusercontent.com/FBNNpxb7m6eM6wtW7MV1Ffp6OXOGLI38q47zcvP29OCYA1yhYH5mZzl5itZi0TgOyZpG', + // handle: LandingPage.incomingCallData.name, + type: 1, + textAccept: 'Accept', + textDecline: 'Decline', + missedCallNotification: NotificationParams( + showNotification: true, + isShowCallback: false, + subtitle: 'Missed call', + callbackText: '', + ), + callingNotification: const NotificationParams( + showNotification: false, + isShowCallback: true, + subtitle: 'Calling...', + callbackText: 'Hang Up', + ), + duration: 30000, + extra: {'userId': '1a2b3c4d'}, + headers: {'apiKey': 'Abc@123!', 'platform': 'flutter'}, + android: const AndroidParams( + isImportant: true, + isShowFullLockedScreen: true, + isCustomNotification: true, + isShowLogo: false, + logoUrl: "https://play-lh.googleusercontent.com/FBNNpxb7m6eM6wtW7MV1Ffp6OXOGLI38q47zcvP29OCYA1yhYH5mZzl5itZi0TgOyZpG", + ringtonePath: 'system_ringtone_default', + backgroundColor: '#ED1C2B', + backgroundUrl: 'https://saudipedia.com/en/saudipediaen/uploads/images/2024/08/02/97358.jpg', + actionColor: '#4CAF50', + textColor: '#000000', + incomingCallNotificationChannelName: "Incoming Call", + missedCallNotificationChannelName: "Missed Call", + isShowCallID: false), + ios: IOSParams( + iconName: 'Meena Health', + handleType: 'generic', + supportsVideo: true, + maximumCallGroups: 2, + maximumCallsPerCallGroup: 1, + audioSessionMode: 'default', + audioSessionActive: true, + audioSessionPreferredSampleRate: 44100.0, + audioSessionPreferredIOBufferDuration: 0.005, + supportsDTMF: true, + supportsHolding: true, + supportsGrouping: false, + supportsUngrouping: false, + ringtonePath: 'system_ringtone_default', + ), + ); + FlutterCallkitIncoming.unsilenceEvents(); + await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); + await Future.delayed(Duration(milliseconds: 500)); } @@ -224,7 +292,9 @@ class PushNotificationHandler { print('🎈 example: onDidAcceptIncomingCall $uuid - $callerId'); await voIPKit.acceptIncomingCall(callerState: CallStateType.calling); await voIPKit.callConnected(); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(Duration(seconds: 2)).then((val) async { + await voIPKit.endCall(); + }); // Navigator.pushNamed( // locator().navigatorKey.currentContext!, @@ -232,8 +302,6 @@ class PushNotificationHandler { // arguments: CallArguments("hoover-dam", "123", "Patient", "40", "1", false), // ); - await voIPKit.endCall(); - // Navigator.pushNamed(navigatorKey.currentContext!, VIDEO_CALL_SCREEN, // arguments: VideoArgus( // reservationId: int.parse(callerId), token: null, isVideo: true)); @@ -250,7 +318,7 @@ class PushNotificationHandler { if (fcmToken != null) onToken(fcmToken); // } } catch (ex) { - print("Notification Exception: " + ex.toString()); + print("Notification Exception: $ex"); } FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); } @@ -258,7 +326,7 @@ class PushNotificationHandler { if (Platform.isIOS) { final permission = await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.getAPNSToken().then((value) async { - log("APNS token: " + value.toString()); + log("APNS token: $value"); await Utils.saveStringFromPrefs(CacheConst.apnsToken, value.toString()); }); await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( @@ -305,14 +373,14 @@ class PushNotificationHandler { }); FirebaseMessaging.instance.getToken().then((String? token) { - print("Push Notification getToken: " + token!); + print("Push Notification getToken: ${token!}"); onToken(token!); }).catchError((err) { print(err); }); FirebaseMessaging.instance.onTokenRefresh.listen((fcm_token) { - print("Push Notification onTokenRefresh: " + fcm_token); + print("Push Notification onTokenRefresh: $fcm_token"); onToken(fcm_token); }); @@ -328,7 +396,7 @@ class PushNotificationHandler { } newMessage(RemoteMessage remoteMessage) async { - print("Remote Message: " + remoteMessage.data.toString()); + print("Remote Message: ${remoteMessage.data}"); if (remoteMessage.data.isEmpty) { return; } @@ -354,7 +422,7 @@ class PushNotificationHandler { } onToken(String token) async { - print("Push Notification Token: " + token); + print("Push Notification Token: $token"); await Utils.saveStringFromPrefs(CacheConst.pushToken, token); } @@ -368,9 +436,7 @@ class PushNotificationHandler { Future requestPermissions() async { try { if (Platform.isIOS) { - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() - ?.requestPermissions(alert: true, badge: true, sound: true); + await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { Map statuses = await [ Permission.notification, diff --git a/lib/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index a4ea936..e57039c 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; @@ -96,6 +98,7 @@ class RequestUtils { request.patientIdentificationID = request.nationalID = (registeredData.patientIdentificationId ?? 0); request.dob = registeredData.dob; request.isRegister = registeredData.isRegister; + log("nationIdText: ${nationIdText}"); } else { if (fileNo) { request.patientID = patientId ?? int.parse(nationIdText); @@ -199,7 +202,8 @@ class RequestUtils { return request; } - static dynamic getUserSignupCompletionRequest({String? fullName, String? emailAddress, GenderTypeEnum? gender, MaritalStatusTypeEnum? maritalStatus}) { + static dynamic getUserSignupCompletionRequest( + {String? fullName, String? emailAddress, GenderTypeEnum? gender, MaritalStatusTypeEnum? maritalStatus}) { AppState appState = getIt.get(); bool isDubai = appState.getUserRegistrationPayload.patientOutSa == 1 ? true : false; @@ -215,11 +219,19 @@ class RequestUtils { return { "Patientobject": { "TempValue": true, - "PatientIdentificationType": - (isDubai ? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1) : appState.getNHICUserData.idNumber!.substring(0, 1)) == "1" ? 1 : 2, - "PatientIdentificationNo": isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), + "PatientIdentificationType": (isDubai + ? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1) + : appState.getNHICUserData.idNumber!.substring(0, 1)) == + "1" + ? 1 + : 2, + "PatientIdentificationNo": + isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), "MobileNumber": appState.getUserRegistrationPayload.patientMobileNumber ?? 0, - "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || appState.getUserRegistrationPayload.zipCode == '+966') ? 0 : 1, + "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || + appState.getUserRegistrationPayload.zipCode == '+966') + ? 0 + : 1, "FirstNameN": isDubai ? "..." : appState.getNHICUserData.firstNameAr, "FirstName": isDubai ? (names.isNotEmpty ? names[0] : "...") : appState.getNHICUserData.firstNameEn, "MiddleNameN": isDubai ? "..." : appState.getNHICUserData.secondNameAr, @@ -233,7 +245,10 @@ class RequestUtils { "eHealthIDField": isDubai ? null : appState.getNHICUserData.healthId, "DateofBirthN": date, "EmailAddress": emailAddress, - "SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || appState.getUserRegistrationPayload.zipCode == '+966') ? "1" : "2", + "SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || + appState.getUserRegistrationPayload.zipCode == '+966') + ? "1" + : "2", "PreferredLanguage": appState.getLanguageCode() == "ar" ? (isDubai ? "1" : 1) : (isDubai ? "2" : 2), "Marital": isDubai ? (maritalStatus == MaritalStatusTypeEnum.single @@ -247,20 +262,25 @@ class RequestUtils { ? '1' : '2'), }, - "PatientIdentificationID": isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), + "PatientIdentificationID": + isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), "PatientMobileNumber": appState.getUserRegistrationPayload.patientMobileNumber.toString()[0] == '0' ? appState.getUserRegistrationPayload.patientMobileNumber : '0${appState.getUserRegistrationPayload.patientMobileNumber}', "DOB": dob, "IsHijri": appState.getUserRegistrationPayload.isHijri, - "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || appState.getUserRegistrationPayload.zipCode == '+966') ? 0 : 1, + "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || + appState.getUserRegistrationPayload.zipCode == '+966') + ? 0 + : 1, "isDentalAllowedBackend": appState.getUserRegistrationPayload.isDentalAllowedBackend, "ZipCode": appState.getUserRegistrationPayload.zipCode, if (!isDubai) "HealthId": appState.getNHICUserData.healthId, }; } - static Future getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode}) async { + static Future getAddFamilyRequest( + {required String nationalIDorFile, required String mobileNo, required String countryCode}) async { FamilyFileRequest request = FamilyFileRequest(); int? loginType = 0; diff --git a/lib/core/utils/size_config.dart b/lib/core/utils/size_config.dart index 9f9d835..0f8766a 100644 --- a/lib/core/utils/size_config.dart +++ b/lib/core/utils/size_config.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; -import 'package:hmg_patient_app_new/core/cache_consts.dart'; class SizeConfig { static double _blockWidth = 0; diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index fdd0d30..4fdc09c 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math' as math; import 'package:flutter/material.dart'; // These are the Viewport values of your Figma Design. @@ -6,6 +7,16 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your const num figmaDesignWidth = 375; // iPhone X / 12 base width const num figmaDesignHeight = 812; // iPhone X / 12 base height + +extension ConstrainedResponsive on num { + /// Width with max cap for tablets + double get wCapped => isTablet ? math.min( w, this * 1.3) : w; + + /// Height with max cap for tablets + double get hCapped => isTablet ? math.min(h, this * 1.3) : h; +} + + extension ResponsiveExtension on num { double get _screenWidth => SizeUtils.width; @@ -27,7 +38,7 @@ extension ResponsiveExtension on num { double clamp; if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) { // More conservative scaling for tablets and foldables - clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.4 : 1.1; + clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; } else { // Original logic for phones clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2; @@ -68,7 +79,7 @@ extension ResponsiveExtension on num { double get r { double baseScale = (this * _screenWidth) / figmaDesignWidth; - if (_isFoldable) { + if (_isFoldable || isTablet) { // Use the same logic as enhanced width for foldables double scale = _screenWidth / figmaDesignWidth; scale = scale.clamp(0.8, 1.4); diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index a11f285..03ff6b5 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/widgets/loading_dialog.dart'; import 'package:lottie/lottie.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; class Utils { static AppState appState = getIt.get(); @@ -146,8 +147,9 @@ class Utils { ? getMonthArabic(dateTime.month) + " " + dateTime.day.toString() + ", " + dateTime.year.toString() : getMonth(dateTime.month) + " " + dateTime.day.toString() + ", " + dateTime.year.toString(); } + static String getDayMonthYearDateFormatted(DateTime? dateTime) { - if(dateTime == null ) return ""; + if (dateTime == null) return ""; return appState.isArabic() ? "${dateTime.day.toString()} ${getMonthArabic(dateTime.month)}, ${dateTime.year.toString()}" : "${dateTime.day.toString()} ${getMonth(dateTime.month)}, ${dateTime.year.toString()}"; @@ -367,10 +369,11 @@ class Utils { crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: isSmallWidget ? 0.h : 48.h), - Lottie.asset(AppAnimations.noData, repeat: false, reverse: false, frameRate: FrameRate(60), width: width.h, height: height.h, fit: BoxFit.fill), + Lottie.asset(AppAnimations.noData, + repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill), SizedBox(height: 16.h), (noDataText ?? LocaleKeys.noDataAvailable.tr()) - .toText16(weight: FontWeight.w500, color: AppColors.greyTextColor, isCenter: true) + .toText14(weight: FontWeight.w500, color: AppColors.greyTextColor, isCenter: true) .paddingSymmetrical(64.w, 0.h), SizedBox(height: 16.h), callToActionButton @@ -383,7 +386,8 @@ class Utils { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + Lottie.asset(AppAnimations.loadingAnimation, + repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor, isCenter: true), SizedBox(height: 8.h), @@ -391,10 +395,10 @@ class Utils { ).center; } - static Widget getSuccessWidget({String? loadingText}) { + static Widget getSuccessWidget({String? loadingText, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) { return Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: crossAxisAlignment, children: [ Lottie.asset(AppAnimations.checkmark, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), @@ -409,7 +413,8 @@ class Utils { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + Lottie.asset(AppAnimations.errorAnimation, + repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor), SizedBox(height: 8.h), @@ -417,14 +422,21 @@ class Utils { ).center; } - static Widget getWarningWidget({String? loadingText, bool isShowActionButtons = false, Widget? bodyWidget, Function? onConfirmTap, Function? onCancelTap}) { + static Widget getWarningWidget({ + String? loadingText, + bool isShowActionButtons = false, + Widget? bodyWidget, + Function? onConfirmTap, + Function? onCancelTap, + }) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 128.h, height: 128.h, fit: BoxFit.fill), + Lottie.asset(AppAnimations.warningAnimation, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), - (loadingText ?? LocaleKeys.loadingText.tr()).toText14(color: AppColors.blackColor, letterSpacing: 0), + (loadingText ?? LocaleKeys.loadingText.tr()).toText15(color: AppColors.blackColor, letterSpacing: 0), SizedBox(height: 16.h), bodyWidget ?? SizedBox.shrink(), SizedBox(height: 16.h), @@ -742,7 +754,7 @@ class Utils { fit: fit, errorBuilder: errorBuilder ?? (_, __, ___) { - //todo change the error builder icon that it is returning + //todo_section change the error builder icon that it is returning return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon); }, ); @@ -754,10 +766,19 @@ class Utils { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Image.asset(AppAssets.mada, width: 25.h, height: 25.h), - Image.asset(AppAssets.tamara_en, width: 25.h, height: 25.h), + Image.asset( + AppAssets.tamaraEng, + width: 25.h, + height: 25.h, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + debugPrint('Failed to load Tamara PNG in payment methods: $error'); + return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 25.h, height: 25.h, fit: BoxFit.contain); + }, + ), Image.asset(AppAssets.visa, width: 25.h, height: 25.h), - Image.asset(AppAssets.Mastercard, width: 25.h, height: 25.h), - Image.asset(AppAssets.apple_pay, width: 25.h, height: 25.h), + Image.asset(AppAssets.mastercard, width: 25.h, height: 25.h), + Image.asset(AppAssets.applePay, width: 25.h, height: 25.h), ], ); } @@ -792,14 +813,15 @@ class Utils { ); } - static Widget getPaymentAmountWithSymbol2(num habibWalletAmount, - {double iconSize = 14, + static Widget getPaymentAmountWithSymbol2( + num habibWalletAmount, { + double iconSize = 14, double? fontSize, double? letterSpacing, FontWeight? fontWeight, Color iconColor = AppColors.textColor, - Color textColor = AppColors.blackColor, - bool isSaudiCurrency = true, + Color textColor = AppColors.blackColor, + bool isSaudiCurrency = true, bool isExpanded = true, }) { return RichText( @@ -816,7 +838,7 @@ class Utils { style: TextStyle( color: textColor, fontSize: fontSize ?? 32.f, - letterSpacing: letterSpacing??-4, + letterSpacing: letterSpacing ?? -4, fontWeight: fontWeight ?? FontWeight.w600, height: 1), ), @@ -837,7 +859,11 @@ class Utils { static Future checkConnection({bool bypassConnectionCheck = false}) async { if (bypassConnectionCheck) return true; List connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult.contains(ConnectivityResult.mobile) || connectivityResult.contains(ConnectivityResult.wifi)) { + if (connectivityResult.contains(ConnectivityResult.mobile) || + connectivityResult.contains(ConnectivityResult.wifi) || + connectivityResult.contains(ConnectivityResult.ethernet) || + connectivityResult.contains(ConnectivityResult.bluetooth) || + connectivityResult.contains(ConnectivityResult.vpn)) { return true; } else { return false; @@ -860,11 +886,43 @@ class Utils { static Future createFileFromString(String encodedStr, String ext) async { Uint8List bytes = base64.decode(encodedStr); String dir = (await getApplicationDocumentsDirectory()).path; - File file = File("$dir/" + DateTime.now().millisecondsSinceEpoch.toString() + "." + ext); + File file = File("$dir/${DateTime.now().millisecondsSinceEpoch}.$ext"); await file.writeAsBytes(bytes); return file.path; } + ///method to be used to get the text as per the langauge of the application + static String getTextWRTCurrentLanguage(String? englishText, String? arabicText) { + String? text = appState.isArabic() ? arabicText : englishText; + return text ?? ''; + } + + static String formatNumberToInternationalFormat(num number, {String? currencySymbol, int decimalDigit = 0}) { + return NumberFormat.currency(locale: 'en_US', symbol: currencySymbol ?? "", decimalDigits: decimalDigit).format(number); + } + + static PatientDoctorAppointmentList? convertToPatientDoctorAppointmentList(HospitalsModel? hospital) { + if (hospital == null) return null; + return PatientDoctorAppointmentList( + filterName: hospital.name, + distanceInKMs: hospital.distanceInKilometers?.toString(), + projectTopName: hospital.name, + projectBottomName: hospital.name, + model: hospital, + isHMC: hospital.isHMC); + } + + + static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) { + if (item == null) return null; + return HospitalsModel( + name: item.filterName, + nameN: item.filterName, + distanceInKilometers: item.distanceInKMs, + isHMC: item.isHMC, + ); + } + static bool havePrivilege(int id) { bool isHavePrivilege = false; try { @@ -877,33 +935,64 @@ class Utils { return isHavePrivilege; } - ///method to be used to get the text as per the langauge of the application - static String getTextWRTCurrentLanguage( - String? englishText, String? arabicText) { - String? text = appState.isArabic() ? arabicText : englishText; - return text ?? ''; + static void openWebView({required String url}) { + Uri uri = Uri.parse(url); + launchUrl(uri, mode: LaunchMode.inAppBrowserView); } + static Color getCardBorderColor(int currentQueueStatus) { + switch (currentQueueStatus) { + case 0: + return AppColors.ratingColorYellow; + case 1: + return AppColors.primaryRedColor; + case 2: + return AppColors.successColor; + } + return AppColors.textColor; + } - static String formatNumberToInternationalFormat(num number, {String? currencySymbol, int decimalDigit = 0}){ - return NumberFormat.currency( - locale: 'en_US', - symbol: currencySymbol??"", - decimalDigits: decimalDigit) - .format(number); + static Color getCardButtonColor(int currentQueueStatus) { + switch (currentQueueStatus) { + case 0: + return AppColors.textColor.withValues(alpha: 0.08); + case 1: + return AppColors.primaryRedColor; + case 2: + return AppColors.successColor; + } + return AppColors.primaryRedColor; } + static Color getCardButtonTextColor(int currentQueueStatus) { + switch (currentQueueStatus) { + case 0: + return Color(0xFFA2A2A2); + case 1: + return AppColors.whiteColor; + case 2: + return AppColors.whiteColor; + } + return AppColors.primaryRedColor; + } - static PatientDoctorAppointmentList? convertToPatientDoctorAppointmentList( - HospitalsModel? hospital) { - if (hospital == null) return null; - return PatientDoctorAppointmentList( - filterName: hospital.name, - distanceInKMs: hospital.distanceInKilometers?.toString(), - projectTopName: hospital.name, - projectBottomName: hospital.name, - model: hospital, - isHMC: hospital.isHMC); + static String getCardButtonText(int currentQueueStatus, String roomNumber) { + switch (currentQueueStatus) { + case 0: + return "Please wait! you will be called for vital signs".needTranslation; + case 1: + return "Please visit Room $roomNumber for vital signs".needTranslation; + case 2: + return "Please visit Room $roomNumber to the Doctor".needTranslation; + } + return ""; } + static bool isDateToday(DateTime dateToCheck) { + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime checkDate = DateTime(dateToCheck.year, dateToCheck.month, dateToCheck.day); + + return checkDate == today; + } } diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index 7e38300..abba447 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -101,8 +101,7 @@ class ValidationUtils { return regex.hasMatch(id); } - static bool validateUaeRegistration( - {String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) { + static bool validateUaeRegistration({String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) { if (name == null || name.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAValidName.tr(), onOkPressed: onOkPress); return false; @@ -141,8 +140,7 @@ class ValidationUtils { return true; } - static bool isValidatedIdAndPhoneWithCountryValidation( - {String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) { + static bool isValidatedIdAndPhoneWithCountryValidation({String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) { bool isCorrectID = true; if (nationalId == null || nationalId.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAnationalID.tr(), onOkPressed: onOkPress); @@ -171,4 +169,10 @@ class ValidationUtils { } return isCorrectID; } -} + + static bool isNullOrEmpty(String? value) { + return value == null || value + .trim() + .isEmpty; + } +} \ No newline at end of file diff --git a/lib/core/utils/zoom_config.dart b/lib/core/utils/zoom_config.dart new file mode 100644 index 0000000..09370c0 --- /dev/null +++ b/lib/core/utils/zoom_config.dart @@ -0,0 +1,4 @@ +const Map configs = { + 'ZOOM_SDK_KEY': 'b9T74nhfTg-ioP9urm970A', + 'ZOOM_SDK_SECRET': 'KOzmjBNXQ1f4IPHpnngfL29uZvJMufSy2Fk8', +}; diff --git a/lib/extensions/int_extensions.dart b/lib/extensions/int_extensions.dart index 80b3171..75460c5 100644 --- a/lib/extensions/int_extensions.dart +++ b/lib/extensions/int_extensions.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; extension IntExtensions on int { Widget get height => SizedBox(height: toDouble()); diff --git a/lib/extensions/route_extensions.dart b/lib/extensions/route_extensions.dart index 0dcc54b..b76acfa 100644 --- a/lib/extensions/route_extensions.dart +++ b/lib/extensions/route_extensions.dart @@ -17,7 +17,7 @@ extension NavigationExtensions on BuildContext { Navigator.of(this).pop(); } - void navigateTo(Widget page) { + void pushNavigateTo(Widget page) { Navigator.push(this, MaterialPageRoute(builder: (context) => page)); } diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 1a6d1cc..309dde1 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -23,14 +23,15 @@ extension CapExtension on String { extension EmailValidator on String { Widget get toWidget => Text(this); - Widget toText8({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text( + Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => + Text( this, maxLines: maxlines, overflow: textOverflow, style: TextStyle( fontSize: 8.f, fontStyle: fontStyle ?? FontStyle.normal, - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: 0, ), @@ -41,12 +42,14 @@ extension EmailValidator on String { FontWeight? weight, bool isBold = false, bool isUnderLine = false, + bool isCenter = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, double letterSpacing = 0}) => Text( this, + textAlign: isCenter ? TextAlign.center : null, maxLines: maxlines, overflow: textOverflow, style: TextStyle( @@ -189,7 +192,8 @@ extension EmailValidator on String { letterSpacing: letterSpacing, height: height, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), - decoration: isUnderLine ? TextDecoration.underline : null), + decoration: isUnderLine ? TextDecoration.underline : null, + decorationColor: color ?? AppColors.blackColor), ); Widget toText15( @@ -212,37 +216,38 @@ extension EmailValidator on String { decoration: isUnderLine ? TextDecoration.underline : null), ); - Widget toText16({ - Color? color, - bool isUnderLine = false, - bool isBold = false, - bool isCenter = false, - int? maxlines, - double? height, - TextAlign? textAlign, - FontWeight? weight, - TextOverflow? textOverflow, - double? letterSpacing = -0.4, - }) => + Widget toText16( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + int? maxlines, + double? height, + TextAlign? textAlign, + FontWeight? weight, + TextOverflow? textOverflow, + double? letterSpacing = -0.4, + Color decorationColor = AppColors.errorColor}) => Text( this, maxLines: maxlines, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - color: color ?? AppColors.blackColor, - fontSize: 16.f, - letterSpacing: letterSpacing, - height: height, - overflow: textOverflow, - fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), - decoration: isUnderLine ? TextDecoration.underline : null, - ), + color: color ?? AppColors.blackColor, + fontSize: 16.f, + letterSpacing: letterSpacing, + height: height, + overflow: textOverflow, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + decoration: isUnderLine ? TextDecoration.underline : null, + decorationColor: decorationColor), ); Widget toText17({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow}) => Text( @@ -251,39 +256,62 @@ extension EmailValidator on String { this, overflow: textOverflow, style: TextStyle( - fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + fontSize: 18.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText19({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), ); - Widget toText20({Color? color, FontWeight? weight, bool isBold = false}) => Text( + Widget toText20({ + Color? color, + FontWeight? weight, + bool isBold = false, + }) => + Text( this, style: TextStyle( - fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + fontSize: 20.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( this, maxLines: maxlines, style: TextStyle( - color: color ?? AppColors.blackColor, fontSize: 21.f, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + color: color ?? AppColors.blackColor, + fontSize: 21.f, + letterSpacing: -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 1, + color: color ?? AppColors.blackColor, + fontSize: 22.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing??-1, fontWeight: isBold ? FontWeight.bold : fontWeight??FontWeight.normal), + height: 23 / 24, + color: color ?? AppColors.blackColor, + fontSize: 24.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), ); Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing}) => Text( @@ -297,28 +325,36 @@ extension EmailValidator on String { fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); - Widget toText28({Color? color, bool isBold = false, double? height, bool isCenter = false}) => Text( + Widget toText28({Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( height: height ?? 23 / 28, color: color ?? AppColors.blackColor, fontSize: 28.f, - letterSpacing: -1, + letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); - Widget toText32({Color? color, bool isBold = false, bool isCenter = false}) => Text( + Widget toText32({FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 32.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal), ); Widget toText44({Color? color, bool isBold = false}) => Text( this, style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 44.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart index 424aa88..70f10bb 100644 --- a/lib/extensions/widget_extensions.dart +++ b/lib/extensions/widget_extensions.dart @@ -1,9 +1,8 @@ -import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:shimmer/shimmer.dart'; import 'package:sizer/sizer.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -19,7 +18,8 @@ extension WidgetExtensions on Widget { Widget paddingAll(double _value) => Padding(padding: EdgeInsets.all(_value), child: this); - Widget paddingSymmetrical(double horizontal, double vertical) => Padding(padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), child: this); + Widget paddingSymmetrical(double horizontal, double vertical) => + Padding(padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), child: this); Widget paddingOnly({double left = 0.0, double right = 0.0, double top = 0.0, double bottom = 0.0}) => Padding(padding: EdgeInsetsDirectional.only(start: left, end: right, top: top, bottom: bottom), child: this); @@ -99,7 +99,7 @@ extension WidgetExtensions on Widget { bool disablePadding = false, double radius = 20, Color? color, - Color borderColor = AppColors.buttonColor, + Color? borderColor, bool disableWidth = false, bool isAlignment = false}) { return Container( @@ -110,7 +110,7 @@ extension WidgetExtensions on Widget { ), color: color, border: Border.all( - color: borderColor, + color: borderColor ?? Colors.transparent, width: disableWidth ? 2 : 1, ), ), diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index d566996..9b09c43 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -5,10 +5,8 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; -import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; -import 'package:hmg_patient_app_new/features/authentication/models/request_models/check_activation_code_register_request_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -101,10 +99,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { } @override - Future>> checkPatientAuthentication({ - required dynamic checkPatientAuthenticationReq, - String? languageID, - }) async { + Future>> checkPatientAuthentication({required dynamic checkPatientAuthenticationReq, String? languageID}) async { int isOutKsa = (checkPatientAuthenticationReq.zipCode == '966' || checkPatientAuthenticationReq.zipCode == '+966') ? 0 : 1; checkPatientAuthenticationReq.patientOutSA = isOutKsa; try { @@ -149,7 +144,6 @@ class AuthenticationRepoImp implements AuthenticationRepo { sendActivationCodeReq.isDentalAllowedBackend = false; final payload = sendActivationCodeReq.toJson(); if (isFormFamilyFile) { - payload.remove("MobileNo"); payload.remove("NationalID"); payload.remove("SMSSignature"); @@ -265,6 +259,12 @@ class AuthenticationRepoImp implements AuthenticationRepo { newRequest.isDentalAllowedBackend = false; newRequest.forRegisteration = newRequest.isRegister ?? false; newRequest.isRegister = false; + //silent login case removed token and login token + if(newRequest.logInTokenID.isEmpty && newRequest.isSilentLogin == true && (newRequest.loginType==1 || newRequest.loginType==4)) { + newRequest.logInTokenID = null; + newRequest.deviceToken = null; + } + } Map familyRequest = {}; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index bcdacac..b393772 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -27,12 +27,12 @@ import 'package:hmg_patient_app_new/features/authentication/models/resp_models/a import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; -import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -40,6 +40,7 @@ import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; @@ -339,7 +340,7 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pop(); }); }, - activationCode: null, //todo silent login case halded on the repo itself.. + activationCode: null, //todo_section silent login case halded on the repo itself.. ); } } @@ -566,7 +567,7 @@ class AuthenticationViewModel extends ChangeNotifier { if (!_appState.getIsChildLoggedIn) { await medicalVm.getFamilyFiles(status: 0); await medicalVm.getAllPendingRecordsByResponseId(); - _navigationService.popUntilNamed(AppRoutes.landingScreen); + _navigationService.replaceAllRoutesAndNavigateToLanding(); } } else { if (activation.list != null && activation.list!.isNotEmpty) { @@ -586,6 +587,7 @@ class AuthenticationViewModel extends ChangeNotifier { activation.list!.first.bloodGroup = activation.patientBlodType; _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); + _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; } // _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); _appState.setAppAuthToken = activation.authenticationTokenId; @@ -675,7 +677,12 @@ class AuthenticationViewModel extends ChangeNotifier { } Future navigateToHomeScreen() async { - _navigationService.pushAndReplace(AppRoutes.landingScreen); + Navigator.pushAndRemoveUntil( + _navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); } Future navigateToOTPScreen( @@ -852,16 +859,13 @@ class AuthenticationViewModel extends ChangeNotifier { request['isRegister'] = true; _appState.setAppAuthToken = response['LogInTokenID']; if (isPatientOutsideSA(request: response)) { - print("=======OUT SA======="); sendActivationCode( - otpTypeEnum: OTPTypeEnumExtension.fromInt(request["OTP_SendType"]), - nationalIdOrFileNumber: request["PatientIdentificationID"].toString(), - phoneNumber: request["PatientMobileNumber"].toString(), - payload: request, - isForRegister: true, - ); + otpTypeEnum: OTPTypeEnumExtension.fromInt(request["OTP_SendType"]), + nationalIdOrFileNumber: request["PatientIdentificationID"].toString(), + phoneNumber: request["PatientMobileNumber"].toString(), + payload: request, + isForRegister: true); } else { - print("=======IN SA======="); chekUserNHICData(request: request); } } diff --git a/lib/features/blood_donation/blood_donation_repo.dart b/lib/features/blood_donation/blood_donation_repo.dart new file mode 100644 index 0000000..dce0975 --- /dev/null +++ b/lib/features/blood_donation/blood_donation_repo.dart @@ -0,0 +1,96 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_response_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/cities_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class BloodDonationRepo { + Future>>> getAllCities(); + + Future>> getPatientBloodGroupDetails(); +} + +class BloodDonationRepoImp implements BloodDonationRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + BloodDonationRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getAllCities() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_CITIES_REQUEST, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListCities']; + final citiesList = list.map((item) => CitiesModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: citiesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getPatientBloodGroupDetails() async { + Map mapDevice = {}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_BLOOD_REQUEST, + body: mapDevice, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_BloodGroupDetails'][0]; + final patientBloodGroup = List_BloodGroupDetailsModel.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: patientBloodGroup, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/features/blood_donation/blood_donation_view_model.dart b/lib/features/blood_donation/blood_donation_view_model.dart new file mode 100644 index 0000000..b8f0e9c --- /dev/null +++ b/lib/features/blood_donation/blood_donation_view_model.dart @@ -0,0 +1,147 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_list_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_response_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/cities_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; + +class BloodDonationViewModel extends ChangeNotifier { + final DialogService dialogService; + BloodDonationRepo bloodDonationRepo; + ErrorHandlerService errorHandlerService; + final NavigationService navigationService; + final AppState appState; + + List citiesList = []; + List bloodGroupList = [ + BloodGroupListModel("O+", 0), + BloodGroupListModel("O-", 1), + BloodGroupListModel("AB+", 2), + BloodGroupListModel("AB-", 3), + BloodGroupListModel("A+", 4), + BloodGroupListModel("A-", 5), + BloodGroupListModel("B+", 6), + BloodGroupListModel("B-", 7), + ]; + + List genderList = [ + BloodGroupListModel(LocaleKeys.malE.tr(), 1), + BloodGroupListModel("Female".needTranslation.tr(), 2), + ]; + + late CitiesModel selectedCity; + late BloodGroupListModel selectedBloodGroup; + int _selectedHospitalIndex = 0; + int _selectedBloodTypeIndex = 0; + String selectedBloodType = ''; + + List_BloodGroupDetailsModel patientBloodGroupDetailsModel = List_BloodGroupDetailsModel(); + + BloodDonationViewModel({required this.bloodDonationRepo, required this.errorHandlerService, required this.navigationService, required this.dialogService, required this.appState}); + + setSelectedCity(CitiesModel city) { + selectedCity = city; + notifyListeners(); + } + + setSelectedBloodGroup(BloodGroupListModel bloodGroup) { + selectedBloodGroup = bloodGroup; + selectedBloodType = selectedBloodGroup.name; + notifyListeners(); + } + + Future getRegionSelectedClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async { + citiesList.clear(); + selectedCity = CitiesModel(); + notifyListeners(); + final result = await bloodDonationRepo.getAllCities(); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? 'An unexpected error occurred'); + } else if (apiResponse.messageStatus == 1) { + citiesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getPatientBloodGroupDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bloodDonationRepo.getPatientBloodGroupDetails(); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? 'An unexpected error occurred'); + } else if (apiResponse.messageStatus == 1) { + patientBloodGroupDetailsModel = apiResponse.data!; + + CitiesModel citiesModel = CitiesModel(); + citiesModel.iD = getSelectedCityID(); + _selectedHospitalIndex = (citiesModel.iD! - 1); + citiesModel.description = citiesList[_selectedHospitalIndex].description; + citiesModel.descriptionN = citiesList[_selectedHospitalIndex].descriptionN; + selectedCity = citiesModel; + selectedBloodType = patientBloodGroupDetailsModel.bloodGroup!; + _selectedBloodTypeIndex = getBloodIndex(selectedBloodType); + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + int getSelectedCityID() { + int cityID = 1; + citiesList.forEach((element) { + if (element.description == patientBloodGroupDetailsModel.city) { + cityID = element.iD!; + } + }); + return cityID; + } + + int getBloodIndex(String type) { + switch (type) { + case "O+": + return 0; + case "O-": + return 1; + case "AB+": + return 2; + case "AB-": + return 3; + case "A+": + return 4; + case "A-": + return 5; + case "B+": + return 6; + case "B-": + return 7; + + default: + return 0; + } + } +} diff --git a/lib/features/blood_donation/models/blood_group_list_model.dart b/lib/features/blood_donation/models/blood_group_list_model.dart new file mode 100644 index 0000000..9a98cc5 --- /dev/null +++ b/lib/features/blood_donation/models/blood_group_list_model.dart @@ -0,0 +1,6 @@ +class BloodGroupListModel { + String name; + int value; + + BloodGroupListModel(this.name, this.value); +} diff --git a/lib/features/blood_donation/models/blood_group_response_model.dart b/lib/features/blood_donation/models/blood_group_response_model.dart new file mode 100644 index 0000000..ceaca78 --- /dev/null +++ b/lib/features/blood_donation/models/blood_group_response_model.dart @@ -0,0 +1,61 @@ +class List_BloodGroupDetailsModel { + int? iD; + int? patientID; + int? patientType; + bool? patientOutSA; + int? zipCode; + String? cellNumber; + String? cityCode; + String? city; + int? gender; + String? bloodGroup; + String? nationalID; + bool? isActive; + + List_BloodGroupDetailsModel({ + this.iD, + this.patientID, + this.patientType, + this.patientOutSA, + this.zipCode, + this.cellNumber, + this.cityCode, + this.city, + this.gender, + this.bloodGroup, + this.nationalID, + this.isActive, + }); + + List_BloodGroupDetailsModel.fromJson(Map json) { + iD = json['ID']; + patientID = json['PatientID']; + patientType = json['PatientType']; + patientOutSA = json['PatientOutSA']; + zipCode = json['ZipCode']; + cellNumber = json['CellNumber']; + cityCode = json['CityCode']; + city = json['City']; + gender = json['Gender']; + bloodGroup = json['BloodGroup']; + nationalID = json['NationalID']; + isActive = json['IsActive']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['PatientID'] = this.patientID; + data['PatientType'] = this.patientType; + data['PatientOutSA'] = this.patientOutSA; + data['ZipCode'] = this.zipCode; + data['CellNumber'] = this.cellNumber; + data['CityCode'] = this.cityCode; + data['City'] = this.city; + data['Gender'] = this.gender; + data['BloodGroup'] = this.bloodGroup; + data['NationalID'] = this.nationalID; + data['IsActive'] = this.isActive; + return data; + } +} diff --git a/lib/features/blood_donation/models/cities_model.dart b/lib/features/blood_donation/models/cities_model.dart new file mode 100644 index 0000000..cf7e631 --- /dev/null +++ b/lib/features/blood_donation/models/cities_model.dart @@ -0,0 +1,21 @@ +class CitiesModel { + int? iD; + String? description; + String? descriptionN; + + CitiesModel({this.iD, this.description, this.descriptionN}); + + CitiesModel.fromJson(Map json) { + iD = json['ID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['Description'] = this.description; + data['DescriptionN'] = this.descriptionN; + return data; + } +} \ No newline at end of file diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 8aafc9b..cfd473e 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; @@ -6,6 +5,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; @@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'models/resp_models/laser_body_parts.dart'; @@ -25,9 +26,12 @@ abstract class BookAppointmentsRepo { Future>>> getDoctorsList(int clinicID, int projectID, bool isNearest, int doctorId, String doctorName, {isContinueDentalPlan = false}); + Future>>> getDoctorsListByHealthCal(int calculationID); + Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); + Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, + {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); Future>> cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}); @@ -82,8 +86,25 @@ abstract class BookAppointmentsRepo { Future>>> getDentalChiefComplaintDoctorsList(int projectID, int chiefComplaintID, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, - {Function(dynamic)? onSuccess, Function(String)? onError}); + Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> checkScannedNFCAndQRCode(String nfcCode, int projectId, {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> getPatientShareWalkInAppointment({required int projectID, required int clinicID, required int doctorID}); + + Future>> insertSpecificAppointmentForWalkIn( + {required int docID, + required int clinicID, + required int projectID, + required String selectedTime, + required String selectedDate, + required int initialSlotDuration, + required int genderID, + required int userAge, + Function(dynamic)? onSuccess, + Function(String)? onError}); + + Future>> getAppointmentNearestGate({required int projectID, required int clinicID}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -189,6 +210,45 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { } } + @override + Future>>> getDoctorsListByHealthCal(int calculationID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = {"CalculationID": calculationID}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_DOCTOR_LIST_CALCULATION, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_CalculationTable']; + + final doctorsList = list.map((item) => DoctorsListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: doctorsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + @override Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}) async { Map mapDevice = { @@ -807,7 +867,8 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { } @override - Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { + Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, + {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { Map mapDevice = { "LaserCategoryID": laserCategoryID, "ProjectID": projectID, @@ -848,4 +909,180 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> checkScannedNFCAndQRCode(String nfcCode, int projectId, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = {"NFC_Code": nfcCode, "ProjectID": projectId}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + CHECK_SCANNED_NFC_QR_CODE, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getPatientShareWalkInAppointment({required int projectID, required int clinicID, required int doctorID}) async { + Map mapRequest = {"ProjectID": projectID, "ClinicID": clinicID, "DoctorID": doctorID}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_PATIENT_SHARE_FOR_WALKIN_APPOINTMENT, + body: mapRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['OnlineCheckInAppointmentsWalkInModel']; + if (list == null || list.isEmpty) { + throw Exception("patient share list is empty"); + } + + final patientShareObj = PatientAppointmentShareResponseModel.fromJson(list); + patientShareObj.isCash = response["IsCash"]; + patientShareObj.isEligible = response["IsEligible"]; + patientShareObj.isInsured = response["IsInsured"]; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: patientShareObj, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> insertSpecificAppointmentForWalkIn( + {required int docID, + required int clinicID, + required int projectID, + required String selectedTime, + required String selectedDate, + required int initialSlotDuration, + required int genderID, + required int userAge, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + Map mapDevice = { + "IsForLiveCare": true, + "ProjectID": projectID, + "ClinicID": clinicID, + "DoctorID": docID, + "StartTime": selectedTime, + "SelectedTime": selectedTime, + "EndTime": selectedTime, + "InitialSlotDuration": initialSlotDuration, + "StrAppointmentDate": selectedDate, + "IsVirtual": false, + "BookedBy": 102, + "VisitType": 1, + "VisitFor": 1, + "GenderID": genderID, + "Age": userAge + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + INSERT_WALKIN_APPOINTMENT, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final appointmentNo = response['AppointmentNo']; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: response["ErrorEndUserMessage"], + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getAppointmentNearestGate({required int projectID, required int clinicID}) async { + Map mapRequest = {"ProjectID": projectID, "ClinicID": clinicID}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_APPOINTMENT_NEAREST_GATE, + body: mapRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final nearestGateResponse = AppointmentNearestGateResponseModel.fromJson(response['getGateByProjectIDandClinicIDList'][0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: nearestGateResponse, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index f24766b..cbed940 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; @@ -15,6 +14,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; @@ -25,6 +25,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots. import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; @@ -44,11 +45,17 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isDoctorsListLoading = false; bool isDoctorProfileLoading = false; bool isDoctorSearchByNameStarted = false; + bool isAppointmentNearestGateLoading = false; bool isLiveCareSchedule = false; + bool isGetDocForHealthCal = false; + int? calculationID = 0; + bool isSortByClinic = true; int initialSlotDuration = 0; + bool isNearestAppointmentSelected = false; + LocationUtils locationUtils; List clinicsList = []; @@ -61,6 +68,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { List doctorsList = []; List filteredDoctorList = []; + // Grouped doctors lists + List> doctorsListByClinic = []; + List> doctorsListByHospital = []; + List> doctorsListGrouped = []; + List liveCareDoctorsList = []; List patientDentalPlanEstimationList = []; @@ -105,8 +117,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { List searchedRegionList = []; List facilityList = ["hmgHospitals", "hmcMedicalClinic"]; List searchedHospitalList = []; - List - searchedPatientDoctorAppointmentHospitalsList = []; + List searchedPatientDoctorAppointmentHospitalsList = []; List searchedClinicList = []; PatientDoctorAppointmentList? selectedHospitalForFilters; @@ -114,15 +125,25 @@ class BookAppointmentsViewModel extends ChangeNotifier { String? selectedClinicForFilters; bool applyFilters = false; + bool isWaitingAppointmentAvailable = false; + + bool isWaitingAppointmentSelected = false; + int waitingAppointmentProjectID = 0; + DoctorsListResponseModel? waitingAppointmentDoctor; + String waitingAppointmentNFCCode = ""; + + PatientAppointmentShareResponseModel? patientWalkInAppointmentShareResponseModel; + + AppointmentNearestGateResponseModel? appointmentNearestGateResponseModel; ///variables for laser clinic - List femaleLaserCategory = [ + List femaleLaserCategory = [ LaserCategoryType(1, 'bodyString'), LaserCategoryType(2, 'face'), - LaserCategoryType(10,'bikini'), + LaserCategoryType(10, 'bikini'), LaserCategoryType(11, 'retouch'), ]; - List maleLaserCategory =[ + List maleLaserCategory = [ LaserCategoryType(1, 'body'), LaserCategoryType(2, 'face'), LaserCategoryType(11, 'retouch'), @@ -137,8 +158,37 @@ class BookAppointmentsViewModel extends ChangeNotifier { int duration = 0; + setIsSortByClinic(bool value) { + isSortByClinic = value; + doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; + notifyListeners(); + } + + // Group doctors by clinic and hospital + void _groupDoctorsList() { + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doctor in doctorsList) { + final clinicKey = (doctor.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doctor); + + final hospitalKey = (doctor.projectName ?? 'Unknown').trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doctor); + } + + doctorsListByClinic = clinicMap.values.toList(); + doctorsListByHospital = hospitalMap.values.toList(); + doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; + } + BookAppointmentsViewModel( - {required this.bookAppointmentsRepo, required this.errorHandlerService, required this.navigationService, required this.myAppointmentsViewModel, required this.locationUtils, required this.dialogService }) { + {required this.bookAppointmentsRepo, + required this.errorHandlerService, + required this.navigationService, + required this.myAppointmentsViewModel, + required this.locationUtils, + required this.dialogService}) { initBookAppointmentViewModel(); } @@ -170,11 +220,44 @@ class BookAppointmentsViewModel extends ChangeNotifier { dentalChiefComplaintsList.clear(); isContinueDentalPlan = false; isChiefComplaintsListLoading = true; + isWaitingAppointmentSelected = false; bodyTypes = [maleLaserCategory, femaleLaserCategory]; // getLocation(); notifyListeners(); } + setIsNearestAppointmentSelected(bool isNearestAppointmentSelected) { + this.isNearestAppointmentSelected = isNearestAppointmentSelected; + + if (isNearestAppointmentSelected) { + doctorsList.sort((a, b) => DateUtil.convertStringToDate(a.nearestFreeSlot!).compareTo(DateUtil.convertStringToDate(b.nearestFreeSlot!))); + } else { + doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); + } + + notifyListeners(); + } + + setIsWaitingAppointmentSelected(bool isWaitingAppointmentSelected) { + this.isWaitingAppointmentSelected = isWaitingAppointmentSelected; + notifyListeners(); + } + + setWaitingAppointmentDoctor(DoctorsListResponseModel waitingAppointmentDoctor) { + this.waitingAppointmentDoctor = waitingAppointmentDoctor; + notifyListeners(); + } + + setWaitingAppointmentNFCCode(String waitingAppointmentNFCCode) { + this.waitingAppointmentNFCCode = waitingAppointmentNFCCode; + notifyListeners(); + } + + setWaitingAppointmentProjectID(int projectID) { + waitingAppointmentProjectID = projectID; + notifyListeners(); + } + setIsDoctorsListLoading(bool value) { if (value) { doctorsList.clear(); @@ -248,6 +331,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void onTabChanged(int index) { + calculationID = null; + isGetDocForHealthCal = false; selectedTabIndex = index; notifyListeners(); } @@ -333,16 +418,49 @@ class BookAppointmentsViewModel extends ChangeNotifier { } //TODO: Make the API dynamic with parameters for ProjectID, isNearest, languageID, doctorId, doctorName - Future getDoctorsList({int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getDoctorsList({int projectID = 0, bool isNearest = true, int doctorId = 0, String doctorName = "", Function(dynamic)? onSuccess, Function(String)? onError}) async { doctorsList.clear(); projectID = currentlySelectedHospitalFromRegionFlow != null ? int.parse(currentlySelectedHospitalFromRegionFlow!) : projectID; - final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, isNearest, doctorId, doctorName, isContinueDentalPlan: isContinueDentalPlan); + final result = + await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, doctorName.isNotEmpty ? false : isNearest, doctorId, doctorName, isContinueDentalPlan: isContinueDentalPlan); result.fold( (failure) async { isDoctorsListLoading = false; + if (onError != null) onError("No doctors found for the search criteria".needTranslation); + + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + doctorsList = apiResponse.data!; + filteredDoctorList = doctorsList; + isDoctorsListLoading = false; + doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); + initializeFilteredList(); + clearSearchFilters(); + getFiltersFromDoctorList(); + _groupDoctorsList(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + //TODO: GetDockets & Calculations For Health Calculator + Future getDoctorsListByHealthCal({Function(dynamic)? onSuccess, Function(String)? onError}) async { + doctorsList.clear(); + final result = await bookAppointmentsRepo.getDoctorsListByHealthCal(calculationID!); + result.fold( + (failure) async { + isDoctorsListLoading = false; + if (onError != null) onError("No doctors found for the search criteria".needTranslation); - onError!("No doctors found for the search criteria".needTranslation); notifyListeners(); }, (apiResponse) { @@ -350,11 +468,13 @@ class BookAppointmentsViewModel extends ChangeNotifier { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { doctorsList = apiResponse.data!; + setIsSortByClinic(true); filteredDoctorList = doctorsList; isDoctorsListLoading = false; initializeFilteredList(); clearSearchFilters(); getFiltersFromDoctorList(); + _groupDoctorsList(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -374,10 +494,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, isNearest, doctorId, doctorName); result.fold( - (failure) async { + (failure) async { onError?.call("No doctors found for the search criteria".needTranslation); }, - (apiResponse) async { + (apiResponse) async { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { @@ -451,6 +571,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { } initialSlotDuration = apiResponse.data["InitialSlotDuration"]; freeSlotsResponse = apiResponse.data['FreeTimeSlots']; + isWaitingAppointmentAvailable = apiResponse.data["IsAllowToBookWaitingAppointment"]; + // isWaitingAppointmentAvailable = true; + freeSlotsResponse.forEach((element) { // date = (isLiveCareSchedule != null && isLiveCareSchedule) // ? DateUtil.convertStringToDate(element) @@ -857,11 +980,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void setSelections( - List? selectedFacilityForFilters, - List? selectedRegionForFilters, - String? selectedClinicForFilters, - PatientDoctorAppointmentList? selectedHospitalForFilters, - bool applyFilters) { + List? selectedFacilityForFilters, List? selectedRegionForFilters, String? selectedClinicForFilters, PatientDoctorAppointmentList? selectedHospitalForFilters, bool applyFilters) { this.selectedFacilityForFilters = selectedFacilityForFilters; this.selectedClinicForFilters = selectedClinicForFilters; this.selectedHospitalForFilters = selectedHospitalForFilters; @@ -872,17 +991,14 @@ class BookAppointmentsViewModel extends ChangeNotifier { void getFiltersFromDoctorList() { doctorsList.forEach((element) { - if (!searchedRegionList - .contains(element.getRegionName(_appState.isArabic()))) { - searchedRegionList - .add(element.getRegionName(_appState.isArabic()) ?? ""); + if (!searchedRegionList.contains(element.getRegionName(_appState.isArabic()))) { + searchedRegionList.add(element.getRegionName(_appState.isArabic()) ?? ""); } if (!searchedHospitalList.contains(element.projectName)) { - searchedPatientDoctorAppointmentHospitalsList - .add(PatientDoctorAppointmentList() - ..filterName = element.projectName - ..isHMC = element.isHMC - ..distanceInKMs = "0"); + searchedPatientDoctorAppointmentHospitalsList.add(PatientDoctorAppointmentList() + ..filterName = element.projectName + ..isHMC = element.isHMC + ..distanceInKMs = "0"); searchedHospitalList.add(element.projectName ?? ""); } if (!searchedClinicList.contains(element.clinicName)) { @@ -932,34 +1048,18 @@ class BookAppointmentsViewModel extends ChangeNotifier { List getDoctorListAsPerSelection() { if (!applyFilters) return doctorsList; - if ((selectedRegionForFilters?.isEmpty == true) && - (selectedFacilityForFilters?.isEmpty == true) && - selectedClinicForFilters == null && - selectedHospitalForFilters == null) { + if ((selectedRegionForFilters?.isEmpty == true) && (selectedFacilityForFilters?.isEmpty == true) && selectedClinicForFilters == null && selectedHospitalForFilters == null) { return doctorsList; } var list = doctorsList.where((element) { - var isInSelectedRegion = (selectedRegionForFilters?.isEmpty == true) - ? true - : selectedRegionForFilters - ?.any((region) => region == element.getRegionName(isArabic())); - var shouldApplyFacilityFilter = - (selectedFacilityForFilters?.isEmpty == true) ? false : true; - var isHMC = (selectedFacilityForFilters?.isEmpty == true) - ? true - : selectedFacilityForFilters?.any((item) => item.contains("hmc")); - var isInSelectedClinic = (selectedClinicForFilters == null) - ? true - : selectedClinicForFilters == element.clinicName; - var isInSelectedHospital = (selectedHospitalForFilters == null) - ? true - : element.projectName == selectedHospitalForFilters?.filterName; + var isInSelectedRegion = (selectedRegionForFilters?.isEmpty == true) ? true : selectedRegionForFilters?.any((region) => region == element.getRegionName(isArabic())); + var shouldApplyFacilityFilter = (selectedFacilityForFilters?.isEmpty == true) ? false : true; + var isHMC = (selectedFacilityForFilters?.isEmpty == true) ? true : selectedFacilityForFilters?.any((item) => item.contains("hmc")); + var isInSelectedClinic = (selectedClinicForFilters == null) ? true : selectedClinicForFilters == element.clinicName; + var isInSelectedHospital = (selectedHospitalForFilters == null) ? true : element.projectName == selectedHospitalForFilters?.filterName; var facilityFilter = ((shouldApplyFacilityFilter == true) ? isHMC : true); - return (isInSelectedRegion ?? true) && - (facilityFilter ?? true) && - isInSelectedClinic && - isInSelectedHospital; + return (isInSelectedRegion ?? true) && (facilityFilter ?? true) && isInSelectedClinic && isInSelectedHospital; }).toList(); return list; } @@ -1042,6 +1142,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { // initializeFilteredList(); // clearSearchFilters(); // getFiltersFromDoctorList(); + _groupDoctorsList(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -1051,7 +1152,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { ); } - setBodyType(int bodyType){ + setBodyType(int bodyType) { selectedBodyTypeIndex = bodyType; selectedCategory = 0; selectedBodyPartList = []; @@ -1059,33 +1160,33 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - FutureOr getLaserClinic() async{ + FutureOr getLaserClinic() async { isBodyPartsLoading = true; int id = bodyTypes[selectedBodyTypeIndex][selectedCategory].laserCategoryID; int projectID = currentlySelectedHospitalFromRegionFlow != null ? int.parse(currentlySelectedHospitalFromRegionFlow!) : 0; int languageID = _appState.isArabic() ? 1 : 0; final result = await bookAppointmentsRepo.getLaserClinics(id, projectID, languageID); result.fold( - (failure) { + (failure) { isBodyPartsLoading = false; notifyListeners(); }, - (apiResponse) {isBodyPartsLoading = false; + (apiResponse) { + isBodyPartsLoading = false; if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { - List response =apiResponse.data!; - if(response.first.category == 2 || response.first.category == 10 ) response.remove(response.first); + List response = apiResponse.data!; + if (response.first.category == 2 || response.first.category == 10) response.remove(response.first); laserBodyPartsList = response; } - notifyListeners(); - + notifyListeners(); }, ); } int getDuration() { - var duration = 0; + var duration = 0; var lowerUpperLegsList = selectedBodyPartList.where((element) => element.mappingCode == "47" || element.mappingCode == "48")?.toList() ?? []; var upperLowerArmsList = selectedBodyPartList.where((element) => element.mappingCode == "40" || element.mappingCode == "41")?.toList() ?? []; @@ -1110,21 +1211,25 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void setSelectedBodyPart(LaserBodyPart part) { - if(selectedBodyPartList.contains(part)){ + if (selectedBodyPartList.contains(part)) { selectedBodyPartList.remove(part); this.duration = getDuration(); notifyListeners(); } else { - if(this.duration == 90){ - dialogService.showErrorBottomSheet(message: "Duration can not exceed 90 min".needTranslation,); + if (this.duration == 90) { + dialogService.showErrorBottomSheet( + message: "Duration can not exceed 90 min".needTranslation, + ); return; } selectedBodyPartList.add(part); var duration = getDuration(); - if(duration > 90){ + if (duration > 90) { selectedBodyPartList.remove(part); - dialogService.showErrorBottomSheet(message: "Duration Exceeds 90 min".needTranslation,); + dialogService.showErrorBottomSheet( + message: "Duration Exceeds 90 min".needTranslation, + ); return; } this.duration = duration; @@ -1137,10 +1242,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { } String getLaserProcedureNameWRTLanguage(LaserBodyPart part) { - if(_appState.isArabic()){ - return part.bodyPartN??""; - }else { - return part.bodyPart??""; + if (_appState.isArabic()) { + return part.bodyPartN ?? ""; + } else { + return part.bodyPart ?? ""; } } @@ -1151,4 +1256,123 @@ class BookAppointmentsViewModel extends ChangeNotifier { laserBodyPartsList = []; duration = 0; } + + Future checkScannedNFCAndQRCode(String nfcCode, int projectId, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bookAppointmentsRepo.checkScannedNFCAndQRCode(nfcCode, projectId); + + result.fold( + (failure) async { + onError!("Invalid verification point scanned.".needTranslation); + }, + (apiResponse) { + // if (apiResponse.data['returnValue'] == 0) { + // onError!("Invalid verification point scanned.".needTranslation); + // } else if (apiResponse.data['returnValue'] == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + // } + }, + ); + } + + Future getWalkInPatientShareAppointment({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bookAppointmentsRepo.getPatientShareWalkInAppointment(projectID: waitingAppointmentProjectID, clinicID: selectedDoctor.clinicID!, doctorID: selectedDoctor.doctorID!); + + result.fold( + (failure) async { + if (onError != null) { + onError(failure.message); + } + // await errorHandlerService.handleError( + // failure: failure, + // onOkPressed: () { + // onError!(failure.message); + // }); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientWalkInAppointmentShareResponseModel = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future insertSpecificAppointmentForWalkIn({Function(dynamic p1)? onSuccess, Function(dynamic p1)? onError}) async { + _appState = getIt(); + final result = await bookAppointmentsRepo.insertSpecificAppointmentForWalkIn( + docID: selectedDoctor.doctorID!, + clinicID: selectedDoctor.clinicID!, + projectID: selectedDoctor.projectID!, + selectedDate: selectedAppointmentDate, + selectedTime: selectedAppointmentTime, + initialSlotDuration: initialSlotDuration, + genderID: _appState.getAuthenticatedUser()!.gender!, + userAge: _appState.getAuthenticatedUser()!.age!, + onError: onError); + + result.fold( + (failure) async { + print(failure); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + LoadingUtils.hideFullScreenLoader(); + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), + navigationService.navigatorKey.currentContext!, + child: Utils.getErrorWidget(loadingText: apiResponse.errorMessage), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data == null || apiResponse.data!.isEmpty) { + onError!("Unexpected Error Occurred".needTranslation); + return; + } + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getAppointmentNearestGate({required int projectID, required int clinicID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + isAppointmentNearestGateLoading = true; + notifyListeners(); + + final result = await bookAppointmentsRepo.getAppointmentNearestGate(projectID: projectID, clinicID: clinicID); + + result.fold( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + appointmentNearestGateResponseModel = apiResponse.data!; + isAppointmentNearestGateLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart b/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart new file mode 100644 index 0000000..bdaa4e2 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart @@ -0,0 +1,64 @@ +class AppointmentNearestGateResponseModel { + String? clinicDescription; + String? clinicDescriptionN; + int? clinicID; + String? clinicLocation; + String? clinicLocationN; + int? gender; + int? iD; + String? nearestGateNumber; + String? nearestGateNumberN; + int? projectID; + String? projectName; + String? projectNameN; + int? rowID; + + AppointmentNearestGateResponseModel( + {this.clinicDescription, + this.clinicDescriptionN, + this.clinicID, + this.clinicLocation, + this.clinicLocationN, + this.gender, + this.iD, + this.nearestGateNumber, + this.nearestGateNumberN, + this.projectID, + this.projectName, + this.projectNameN, + this.rowID}); + + AppointmentNearestGateResponseModel.fromJson(Map json) { + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + clinicID = json['ClinicID']; + clinicLocation = json['ClinicLocation']; + clinicLocationN = json['ClinicLocationN']; + gender = json['Gender']; + iD = json['ID']; + nearestGateNumber = json['NearestGateNumber']; + nearestGateNumberN = json['NearestGateNumberN']; + projectID = json['ProjectID']; + projectName = json['ProjectName']; + projectNameN = json['ProjectNameN']; + rowID = json['RowID']; + } + + Map toJson() { + final Map data = Map(); + data['ClinicDescription'] = clinicDescription; + data['ClinicDescriptionN'] = clinicDescriptionN; + data['ClinicID'] = clinicID; + data['ClinicLocation'] = clinicLocation; + data['ClinicLocationN'] = clinicLocationN; + data['Gender'] = gender; + data['ID'] = iD; + data['NearestGateNumber'] = nearestGateNumber; + data['NearestGateNumberN'] = nearestGateNumberN; + data['ProjectID'] = projectID; + data['ProjectName'] = projectName; + data['ProjectNameN'] = projectNameN; + data['RowID'] = rowID; + return data; + } +} diff --git a/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart index 5df6ed8..75ed122 100644 --- a/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart +++ b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart @@ -65,75 +65,77 @@ class DoctorsListResponseModel { String? regionID; String? projectBottomName; String? projectTopName; + int? calcID; - - DoctorsListResponseModel( - {this.clinicID, - this.clinicName, - this.clinicNameN, - this.doctorTitle, - this.iD, - this.name, - this.projectID, - this.projectName, - this.actualDoctorRate, - this.clinicRoomNo, - this.date, - this.dayName, - this.decimalDoctorRate, - this.doctorAvailability, - this.doctorID, - this.doctorImageURL, - this.doctorMobileNumber, - this.doctorProfile, - this.doctorProfileInfo, - this.doctorRate, - this.doctorStarsRate, - this.employmentType, - this.gender, - this.genderDescription, - this.hISRegionId, - this.isActive, - this.isAllowWaitList, - this.isAppointmentAllowed, - this.isDoctorAllowVedioCall, - this.isDoctorDummy, - this.isDoctorHasPrePostImages, - this.isHMC, - this.isHmg, - this.isLiveCare, - this.latitude, - this.longitude, - this.nationalityFlagURL, - this.nationalityID, - this.nationalityName, - this.nearestFreeSlot, - this.noOfFreeSlotsAvailable, - this.noOfPatientsRate, - this.originalClinicID, - this.personRate, - this.projectDistanceInKiloMeters, - this.projectNameBottom, - this.projectNameTop, - this.qR, - this.qRString, - this.rateNumber, - this.regionName, - this.regionNameN, - this.serviceID, - this.setupID, - this.speciality, - this.specialityN, - this.transactionType, - this.virtualEmploymentType, - this.workingHours, - this.vida3Id, - this.region, - this.regionArabic, - this.regionEnglish, - this.regionID, - this.projectBottomName, - this.projectTopName,}); + DoctorsListResponseModel({ + this.clinicID, + this.clinicName, + this.clinicNameN, + this.doctorTitle, + this.iD, + this.name, + this.projectID, + this.projectName, + this.actualDoctorRate, + this.clinicRoomNo, + this.date, + this.dayName, + this.decimalDoctorRate, + this.doctorAvailability, + this.doctorID, + this.doctorImageURL, + this.doctorMobileNumber, + this.doctorProfile, + this.doctorProfileInfo, + this.doctorRate, + this.doctorStarsRate, + this.employmentType, + this.gender, + this.genderDescription, + this.hISRegionId, + this.isActive, + this.isAllowWaitList, + this.isAppointmentAllowed, + this.isDoctorAllowVedioCall, + this.isDoctorDummy, + this.isDoctorHasPrePostImages, + this.isHMC, + this.isHmg, + this.isLiveCare, + this.latitude, + this.longitude, + this.nationalityFlagURL, + this.nationalityID, + this.nationalityName, + this.nearestFreeSlot, + this.noOfFreeSlotsAvailable, + this.noOfPatientsRate, + this.originalClinicID, + this.personRate, + this.projectDistanceInKiloMeters, + this.projectNameBottom, + this.projectNameTop, + this.qR, + this.qRString, + this.rateNumber, + this.regionName, + this.regionNameN, + this.serviceID, + this.setupID, + this.speciality, + this.specialityN, + this.transactionType, + this.virtualEmploymentType, + this.workingHours, + this.vida3Id, + this.region, + this.regionArabic, + this.regionEnglish, + this.regionID, + this.projectBottomName, + this.projectTopName, + this.calcID, + }); DoctorsListResponseModel.fromJson(Map json) { clinicID = json['ClinicID']; @@ -141,7 +143,7 @@ class DoctorsListResponseModel { clinicNameN = json['ClinicNameN']; doctorTitle = json['DoctorTitle']; iD = json['ID']; - name = json['Name']; + name = json['Name'] ?? json["DoctorName"]; projectID = json['ProjectID']; projectName = json['ProjectName']; actualDoctorRate = json['ActualDoctorRate']; @@ -174,7 +176,7 @@ class DoctorsListResponseModel { longitude = json['Longitude']; nationalityFlagURL = json['NationalityFlagURL']; nationalityID = json['NationalityID']; - nationalityName = json['NationalityName']; + nationalityName = json['NationalityName'] ?? json["Nationality"]; nearestFreeSlot = json['NearestFreeSlot']; noOfFreeSlotsAvailable = json['NoOfFreeSlotsAvailable']; noOfPatientsRate = json['NoOfPatientsRate']; @@ -200,6 +202,7 @@ class DoctorsListResponseModel { regionEnglish = json['RegionName']; projectBottomName = json['ProjectNameBottom']; projectTopName = json['ProjectNameTop']; + calcID = json["CalcID"]; } Map toJson() { @@ -264,6 +267,7 @@ class DoctorsListResponseModel { data['VirtualEmploymentType'] = this.virtualEmploymentType; data['WorkingHours'] = this.workingHours; data['vida3Id'] = this.vida3Id; + data['CalcID'] = this.calcID; return data; } @@ -273,7 +277,8 @@ class DoctorsListResponseModel { } return regionEnglish; } - String getProjectCompleteName(){ + + String getProjectCompleteName() { return "${this.projectTopName} ${this.projectBottomName}"; } diff --git a/lib/features/book_appointments/models/resp_models/get_allergies_response_model.dart b/lib/features/book_appointments/models/resp_models/get_allergies_response_model.dart new file mode 100644 index 0000000..cd4802f --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/get_allergies_response_model.dart @@ -0,0 +1,37 @@ +class GetAllergiesResponseModel { + int? patientID; + int? allergyDiseaseType; + int? allergyDiseaseID; + String? description; + String? descriptionN; + String? remarks; + + GetAllergiesResponseModel({ + this.patientID, + this.allergyDiseaseType, + this.allergyDiseaseID, + this.description, + this.descriptionN, + this.remarks, + }); + + GetAllergiesResponseModel.fromJson(Map json) { + patientID = json['PatientID']; + allergyDiseaseType = json['AllergyDiseaseType']; + allergyDiseaseID = json['AllergyDiseaseID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + remarks = json['Remarks']; + } + + Map toJson() { + final Map data = new Map(); + data['PatientID'] = this.patientID; + data['AllergyDiseaseType'] = this.allergyDiseaseType; + data['AllergyDiseaseID'] = this.allergyDiseaseID; + data['Description'] = this.description; + data['DescriptionN'] = this.descriptionN; + data['Remarks'] = this.remarks; + return data; + } +} diff --git a/lib/features/contact_us/contact_us_repo.dart b/lib/features/contact_us/contact_us_repo.dart new file mode 100644 index 0000000..3e96f91 --- /dev/null +++ b/lib/features/contact_us/contact_us_repo.dart @@ -0,0 +1,143 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/req_models/request_insert_coc_item.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_hmg_locations.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_patient_ic_projects.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class ContactUsRepo { + Future>>> getHMGLocations(); + + Future>>> getLiveChatProjectsList(); + + Future>> insertCOCItem({required RequestInsertCOCItem requestInsertCOCItem, PatientAppointmentHistoryResponseModel? patientSelectedAppointment}); +} + +class ContactUsRepoImp implements ContactUsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + ContactUsRepoImp({required this.apiClient, required this.loggerService}); + + @override + Future>>> getHMGLocations() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_FINDUS_REQUEST, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListHMGLocation']; + final hmgLocations = list.map((item) => GetHMGLocationsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: hmgLocations, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getLiveChatProjectsList() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_LIVECHAT_REQUEST, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_PatientICProjects']; + final liveChatProjectsList = list.map((item) => GetPatientICProjectsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: liveChatProjectsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> insertCOCItem({required RequestInsertCOCItem requestInsertCOCItem, PatientAppointmentHistoryResponseModel? patientSelectedAppointment}) async { + final Map body = requestInsertCOCItem.toJson(); + + if (patientSelectedAppointment != null) { + body['AppoinmentNo'] = patientSelectedAppointment.appointmentNo; + body['AppointmentDate'] = patientSelectedAppointment.appointmentDate; + body['ClinicID'] = patientSelectedAppointment.clinicID; + body['ClinicName'] = patientSelectedAppointment.clinicName; + body['DoctorID'] = patientSelectedAppointment.doctorID; + body['DoctorName'] = patientSelectedAppointment.doctorNameObj; + body['ProjectName'] = patientSelectedAppointment.projectName; + } + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + SEND_FEEDBACK, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart new file mode 100644 index 0000000..1185700 --- /dev/null +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -0,0 +1,198 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_repo.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/req_models/request_insert_coc_item.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_hmg_locations.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_patient_ic_projects.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class ContactUsViewModel extends ChangeNotifier { + ContactUsRepo contactUsRepo; + ErrorHandlerService errorHandlerService; + AppState appState; + + bool isHMGLocationsListLoading = false; + bool isHMGHospitalsListSelected = true; + bool isLiveChatProjectsListLoading = false; + bool isSendFeedbackTabSelected = true; + + List hmgHospitalsLocationsList = []; + List hmgPharmacyLocationsList = []; + + List liveChatProjectsList = []; + + int selectedLiveChatProjectIndex = -1; + + List feedbackAttachmentList = []; + + PatientAppointmentHistoryResponseModel? patientFeedbackSelectedAppointment; + + List feedbackTypeList = [ + FeedbackType(id: 1, nameEN: "Complaint for appointment", nameAR: 'شكوى على موعد'), + FeedbackType(id: 2, nameEN: "Complaint without appointment", nameAR: 'شكوى بدون موعد'), + FeedbackType(id: 3, nameEN: "Question", nameAR: 'سؤال'), + FeedbackType(id: 4, nameEN: "Appreciation", nameAR: 'تقدير'), + FeedbackType(id: 6, nameEN: "Suggestion", nameAR: 'إقتراح'), + FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد'), + ]; + + FeedbackType selectedFeedbackType = FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد'); + + ContactUsViewModel({required this.contactUsRepo, required this.errorHandlerService, required this.appState}); + + initContactUsViewModel() { + isHMGLocationsListLoading = true; + isHMGHospitalsListSelected = true; + isLiveChatProjectsListLoading = true; + hmgHospitalsLocationsList.clear(); + hmgPharmacyLocationsList.clear(); + liveChatProjectsList.clear(); + feedbackAttachmentList.clear(); + selectedFeedbackType = FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد'); + setPatientFeedbackSelectedAppointment(null); + getHMGLocations(); + notifyListeners(); + } + + setHMGHospitalsListSelected(bool isSelected) { + isHMGHospitalsListSelected = isSelected; + notifyListeners(); + } + + setSelectedLiveChatProjectIndex(int index) { + selectedLiveChatProjectIndex = index; + notifyListeners(); + } + + setIsSendFeedbackTabSelected(bool isSelected) { + isSendFeedbackTabSelected = isSelected; + notifyListeners(); + } + + setSelectedFeedbackType(FeedbackType feedbackType) { + selectedFeedbackType = feedbackType; + notifyListeners(); + } + + addFeedbackAttachment(String attachmentPath) { + feedbackAttachmentList.add(attachmentPath); + notifyListeners(); + } + + removeFeedbackAttachment(String attachmentPath) { + feedbackAttachmentList.remove(attachmentPath); + notifyListeners(); + } + + setPatientFeedbackSelectedAppointment(PatientAppointmentHistoryResponseModel? appointment) { + patientFeedbackSelectedAppointment = appointment; + notifyListeners(); + } + + Future getHMGLocations({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isHMGLocationsListLoading = true; + hmgHospitalsLocationsList.clear(); + hmgPharmacyLocationsList.clear(); + notifyListeners(); + + final result = await contactUsRepo.getHMGLocations(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + // hmgLocationsList = apiResponse.data!; + for (var location in apiResponse.data!) { + if (location.locationType == 1) { + hmgHospitalsLocationsList.add(location); + } else if (location.locationType == 2) { + hmgPharmacyLocationsList.add(location); + } + } + isHMGLocationsListLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getLiveChatProjectsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isLiveChatProjectsListLoading = true; + liveChatProjectsList.clear(); + + notifyListeners(); + + final result = await contactUsRepo.getLiveChatProjectsList(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + liveChatProjectsList = apiResponse.data!; + liveChatProjectsList.sort((a, b) => b.distanceInKilometers.compareTo(a.distanceInKilometers)); + isLiveChatProjectsListLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future insertCOCItem({required String subject, required String message, Function(dynamic)? onSuccess, Function(String)? onError}) async { + RequestInsertCOCItem requestInsertCOCItem = RequestInsertCOCItem(); + requestInsertCOCItem.attachment = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first : ""; + requestInsertCOCItem.title = subject; + requestInsertCOCItem.details = message; + requestInsertCOCItem.cOCTypeName = selectedFeedbackType.id.toString(); + requestInsertCOCItem.formTypeID = selectedFeedbackType.id.toString(); + requestInsertCOCItem.mobileNo = "966${Utils.getPhoneNumberWithoutZero(appState.getAuthenticatedUser()!.mobileNumber!)}"; + requestInsertCOCItem.isUserLoggedIn = true; + requestInsertCOCItem.projectID = 0; + requestInsertCOCItem.patientName = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}"; + requestInsertCOCItem.fileName = ""; + requestInsertCOCItem.appVersion = ApiConsts.appVersionID; + requestInsertCOCItem.uILanguage = appState.isArabic() ? "ar" : "en"; //TODO Change it to be dynamic + requestInsertCOCItem.browserInfo = Platform.localHostname; + requestInsertCOCItem.deviceInfo = Platform.localHostname; + requestInsertCOCItem.resolution = "400x847"; + requestInsertCOCItem.projectID = 0; + requestInsertCOCItem.tokenID = "C0c@@dm!n?T&A&A@Barcha202029582948"; + requestInsertCOCItem.identificationNo = int.parse(appState.getAuthenticatedUser()!.patientIdentificationNo!); + if (BASE_URL.contains('uat')) { + requestInsertCOCItem.forDemo = true; + } else { + requestInsertCOCItem.forDemo = false; + } + + final result = await contactUsRepo.insertCOCItem(requestInsertCOCItem: requestInsertCOCItem, patientSelectedAppointment: patientFeedbackSelectedAppointment); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } +} diff --git a/lib/features/contact_us/models/feedback_type.dart b/lib/features/contact_us/models/feedback_type.dart new file mode 100644 index 0000000..ff1025a --- /dev/null +++ b/lib/features/contact_us/models/feedback_type.dart @@ -0,0 +1,11 @@ +class FeedbackType { + final int id; + final String nameEN; + final String nameAR; + + FeedbackType({ + required this.id, + required this.nameEN, + required this.nameAR, + }); +} diff --git a/lib/features/contact_us/models/req_models/request_insert_coc_item.dart b/lib/features/contact_us/models/req_models/request_insert_coc_item.dart new file mode 100644 index 0000000..e285999 --- /dev/null +++ b/lib/features/contact_us/models/req_models/request_insert_coc_item.dart @@ -0,0 +1,137 @@ +class RequestInsertCOCItem { + bool? isUserLoggedIn; + String? mobileNo; + int? identificationNo; + int? patientID; + int? patientOutSA; + int? patientTypeID; + String? tokenID; + String? patientName; + int? projectID; + String? fileName; + String? attachment; + String? uILanguage; + String? browserInfo; + String? cOCTypeName; + String? formTypeID; + String? details; + String? deviceInfo; + String? deviceType; + String? title; + String? resolution; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientType; + double? appVersion; + bool? forDemo; + + RequestInsertCOCItem( + {this.isUserLoggedIn, + this.mobileNo, + this.identificationNo, + this.patientID, + this.patientOutSA, + this.patientTypeID, + this.tokenID, + this.patientName, + this.projectID, + this.fileName, + this.attachment, + this.uILanguage, + this.browserInfo, + this.cOCTypeName, + this.formTypeID, + this.details, + this.deviceInfo, + this.deviceType, + this.title, + this.resolution, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientType, + this.appVersion, + this.forDemo}); + + RequestInsertCOCItem.fromJson(Map json) { + isUserLoggedIn = json['IsUserLoggedIn']; + mobileNo = json['MobileNo']; + identificationNo = json['IdentificationNo']; + patientID = json['PatientID']; + patientOutSA = json['PatientOutSA']; + patientTypeID = json['PatientTypeID']; + tokenID = json['TokenID']; + patientName = json['PatientName']; + projectID = json['ProjectID']; + fileName = json['FileName']; + attachment = json['Attachment']; + uILanguage = json['UILanguage']; + browserInfo = json['BrowserInfo']; + cOCTypeName = json['COCTypeName']; + formTypeID = json['FormTypeID']; + details = json['Details']; + deviceInfo = json['DeviceInfo']; + deviceType = json['DeviceType']; + title = json['Title']; + resolution = json['Resolution']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientType = json['PatientType']; + appVersion = json['AppVersion']; + forDemo = json['ForDemo']; + } + + Map toJson() { + final Map data = new Map(); + data['IsUserLoggedIn'] = this.isUserLoggedIn; + data['MobileNo'] = this.mobileNo; + data['IdentificationNo'] = this.identificationNo; + data['PatientID'] = this.patientID; + data['PatientOutSA'] = this.patientOutSA; + data['PatientTypeID'] = this.patientTypeID; + data['TokenID'] = this.tokenID; + data['PatientName'] = this.patientName; + data['ProjectID'] = this.projectID; + data['FileName'] = this.fileName; + data['Attachment'] = this.attachment; + data['UILanguage'] = this.uILanguage; + data['BrowserInfo'] = this.browserInfo; + data['COCTypeName'] = this.cOCTypeName; + data['FormTypeID'] = this.formTypeID; + data['Details'] = this.details; + data['DeviceInfo'] = this.deviceInfo; + data['DeviceType'] = this.deviceType; + data['Title'] = this.title; + data['Resolution'] = this.resolution; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientType'] = this.patientType; + data['AppVersion'] = this.appVersion; + data['ForDemo'] = this.forDemo; + + return data; + } +} diff --git a/lib/features/contact_us/models/resp_models/get_hmg_locations.dart b/lib/features/contact_us/models/resp_models/get_hmg_locations.dart new file mode 100644 index 0000000..d15b853 --- /dev/null +++ b/lib/features/contact_us/models/resp_models/get_hmg_locations.dart @@ -0,0 +1,80 @@ +class GetHMGLocationsModel { + dynamic cityID; + String? cityName; + dynamic cityNameN; + dynamic distanceInKilometers; + bool? isActive; + String? latitude; + int? locationID; + String? locationName; + dynamic locationNameN; + dynamic locationType; + String? longitude; + int? pharmacyLocationID; + String? phoneNumber; + int? projectID; + String? projectImageURL; + int? setupID; + dynamic sortOrder; + + GetHMGLocationsModel( + {this.cityID, + this.cityName, + this.cityNameN, + this.distanceInKilometers, + this.isActive, + this.latitude, + this.locationID, + this.locationName, + this.locationNameN, + this.locationType, + this.longitude, + this.pharmacyLocationID, + this.phoneNumber, + this.projectID, + this.projectImageURL, + this.setupID, + this.sortOrder}); + + GetHMGLocationsModel.fromJson(Map json) { + cityID = json['CityID']; + cityName = json['CityName']; + cityNameN = json['CityNameN']; + distanceInKilometers = json['DistanceInKilometers']; + isActive = json['IsActive']; + latitude = json['Latitude']; + locationID = json['LocationID']; + locationName = json['LocationName']; + locationNameN = json['LocationNameN']; + locationType = json['LocationType']; + longitude = json['Longitude']; + pharmacyLocationID = json['PharmacyLocationID']; + phoneNumber = json['PhoneNumber']; + projectID = json['ProjectID']; + projectImageURL = json['ProjectImageURL']; + setupID = json['SetupID']; + sortOrder = json['SortOrder']; + } + + Map toJson() { + final Map data = new Map(); + data['CityID'] = this.cityID; + data['CityName'] = this.cityName; + data['CityNameN'] = this.cityNameN; + data['DistanceInKilometers'] = this.distanceInKilometers; + data['IsActive'] = this.isActive; + data['Latitude'] = this.latitude; + data['LocationID'] = this.locationID; + data['LocationName'] = this.locationName; + data['LocationNameN'] = this.locationNameN; + data['LocationType'] = this.locationType; + data['Longitude'] = this.longitude; + data['PharmacyLocationID'] = this.pharmacyLocationID; + data['PhoneNumber'] = this.phoneNumber; + data['ProjectID'] = this.projectID; + data['ProjectImageURL'] = this.projectImageURL; + data['SetupID'] = this.setupID; + data['SortOrder'] = this.sortOrder; + return data; + } +} \ No newline at end of file diff --git a/lib/features/contact_us/models/resp_models/get_patient_ic_projects.dart b/lib/features/contact_us/models/resp_models/get_patient_ic_projects.dart new file mode 100644 index 0000000..ba7463f --- /dev/null +++ b/lib/features/contact_us/models/resp_models/get_patient_ic_projects.dart @@ -0,0 +1,48 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class GetPatientICProjectsModel { + int? id; + String? projectName; + String? projectNameN; + String? value; + dynamic languageId; + DateTime? createdOn; + String? createdBy; + dynamic editedOn; + dynamic editedBy; + bool? isActive; + dynamic distanceInKilometers; + + GetPatientICProjectsModel( + {this.id, this.projectName, this.projectNameN, this.value, this.languageId, this.createdOn, this.createdBy, this.editedOn, this.editedBy, this.distanceInKilometers, this.isActive}); + + GetPatientICProjectsModel.fromJson(Map json) { + id = json['id']; + projectName = json['ProjectName']; + projectNameN = json['ProjectNameN']; + value = json['Value']; + languageId = json['LanguageId']; + createdOn = DateUtil.convertStringToDate(json['CreatedOn']); + createdBy = json['CreatedBy']; + editedOn = json['EditedOn']; + editedBy = json['EditedBy']; + isActive = json['IsActive']; + distanceInKilometers = json['DistanceInKilometers']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['ProjectName'] = this.projectName; + data['ProjectNameN'] = this.projectNameN; + data['Value'] = this.value; + data['LanguageId'] = this.languageId; + data['CreatedOn'] = this.createdOn; + data['CreatedBy'] = this.createdBy; + data['EditedOn'] = this.editedOn; + data['EditedBy'] = this.editedBy; + data['IsActive'] = this.isActive; + data['DistanceInKilometers'] = this.distanceInKilometers; + return data; + } +} diff --git a/lib/features/emergency_services/emergency_services_repo.dart b/lib/features/emergency_services/emergency_services_repo.dart index 65594b8..c63f0ee 100644 --- a/lib/features/emergency_services/emergency_services_repo.dart +++ b/lib/features/emergency_services/emergency_services_repo.dart @@ -1,11 +1,18 @@ +import 'dart:developer'; + import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/ProjectAvgERWaitingTime.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/rrt_procedures_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -13,20 +20,44 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'models/resp_model/PatientERTransportationMethod.dart'; abstract class EmergencyServicesRepo { - Future>>> getRRTProcedures(); + Future>>> getRRTProcedures(int languageId); Future>>> getNearestEr({int? id, int? projectID}); - Future>>> getTransportationMethods({int? id}); + Future>> checkPatientERAdvanceBalance(); Future>>> getProjectList(); + Future>> checkPatientERPaymentInformation({int projectID}); + + Future>> createAdvancePaymentForER( + {required int projectID, + required AuthenticatedUser authUser, + required num paymentAmount, + required String paymentMethodName, + required String paymentReference}); + + Future>> addAdvanceNumberRequest( + {required String advanceNumber, required String paymentReference, required String appointmentNo}); + + Future>> getProjectIDFromNFC({required String nfcCode}); + + Future>> autoGenerateInvoiceERClinic({required int projectID}); + + Future>>> getTransportationMethods({int? id}); + Future>> submitAmbulanceRequest(PatientER_RC request); Future>>> getTransportationOrders({int? id}); Future>> cancelOrder(int? iD, int patientId); + Future>> submitRRTRequest(RRTRequestModel request); + + Future>> getRRTOrders({int? id}); + + Future>> cancelRRTOrder(int? iD); + Future>> getTermsAndCondition(); } class EmergencyServicesRepoImp implements EmergencyServicesRepo { @@ -51,7 +82,8 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { try { final list = response['List_ProjectAvgERWaitingTime']; - final clinicsList = list.map((item) => ProjectAvgERWaitingTime.fromJson(item as Map)).toList().cast(); + final clinicsList = + list.map((item) => ProjectAvgERWaitingTime.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( messageStatus: messageStatus, statusCode: statusCode, @@ -72,8 +104,8 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { } @override - Future>>> getRRTProcedures() async { - Map mapDevice = {}; + Future>>> getRRTProcedures(int languageId) async { + Map mapDevice = {"ProjectID": 15, "languageID":1}; try { GenericApiModel>? apiResponse; @@ -87,7 +119,8 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['Vida_ProcedureList']; - final proceduresList = list.map((item) => RRTProceduresResponseModel.fromJson(item as Map)).toList().cast(); + final proceduresList = + list.map((item) => RRTProceduresResponseModel.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( messageStatus: messageStatus, @@ -124,7 +157,10 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['response']['transportationservices']; - final proceduresList = list.map((item) => PatientERTransportationMethod.fromJson(item as Map)).toList().cast(); + final proceduresList = list + .map((item) => PatientERTransportationMethod.fromJson(item as Map)) + .toList() + .cast(); apiResponse = GenericApiModel>( messageStatus: messageStatus, @@ -223,7 +259,7 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { Failure? failure; await apiClient.post( body: {}, - "$GET_ALL_TRANSPORTATIONS_ORDERS?patientID=$id", + "$GET_ALL_TRANSPORTATIONS_ORDERS?patientID=$id", isRCService: true, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -231,7 +267,8 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['response']; - final proceduresList = list.map((item) => AmbulanceRequestOrdersModel.fromJson(item as Map)).toList().cast(); + final proceduresList = + list.map((item) => AmbulanceRequestOrdersModel.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( messageStatus: messageStatus, @@ -290,4 +327,395 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> checkPatientERAdvanceBalance() async { + Map mapDevice = {"ClinicID": 10}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + CHECK_PATIENT_ER_ADVANCE_BALANCE, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) => failure = failureType, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final bool patientHasERBalance = response['BalanceAmount'] > 0; + log(patientHasERBalance.toString()); + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: patientHasERBalance, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> checkPatientERPaymentInformation({int? projectID}) async { + Map mapDevice = {"ClinicID": 10, "ProjectID": projectID ?? 0}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_ER_ONLINE_PAYMENT_DETAILS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final erOnlineCheckInPaymentDetailsResponse = EROnlineCheckInPaymentDetailsResponse.fromJson(response["ResponsePatientShare"]); + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: erOnlineCheckInPaymentDetailsResponse, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> createAdvancePaymentForER( + {required int projectID, + required AuthenticatedUser authUser, + required num paymentAmount, + required String paymentMethodName, + required String paymentReference}) async { + Map mapDevice = { + "LanguageID": 1, + "ERAdvanceAmount": { + "ProjectId": projectID, + "PatientId": authUser.patientId, + "ClinicId": 10, + "DepositorName": "${authUser.firstName!} ${authUser.lastName!}", + "MemberId": authUser.patientId, + "NationalityID": authUser.nationalityId, + "PaymentAmount": paymentAmount, + "PaymentDate": DateUtil.convertDateToString(DateTime.now()), + "PaymentMethodName": paymentMethodName, + "PaymentReferenceNumber": paymentReference, + "SourceType": 2 + } + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ER_CREATE_ADVANCE_PAYMENT, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final vidaAdvanceNumber = response['ER_AdvancePaymentResponse']['AdvanceNumber'].toString(); + log(vidaAdvanceNumber); + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: vidaAdvanceNumber, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> addAdvanceNumberRequest( + {required String advanceNumber, required String paymentReference, required String appointmentNo}) async { + Map requestBody = { + "AdvanceNumber": advanceNumber, + "AdvanceNumber_VP": advanceNumber, + "PaymentReferenceNumber": paymentReference, + "AppointmentID": appointmentNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ADD_ADVANCE_NUMBER_REQUEST, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getProjectIDFromNFC({required String nfcCode}) async { + Map mapDevice = {"nFC_Code": nfcCode}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_PROJECT_FROM_NFC, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final projectID = response['GetProjectByNFC'][0]["ProjectID"]; + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: projectID, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> autoGenerateInvoiceERClinic({required int projectID}) async { + Map mapDevice = { + "ProjectID": projectID, + "ClinicID": "10", + "IsAdvanceAvailable": true, + "MemberID": 102, + "PaymentMethod": "VISA", + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + AUTO_GENERATE_INVOICE_ER, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> submitRRTRequest(RRTRequestModel request) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + body: request.toJson(), + ADD_RRT_ORDER_RC, + isRCService: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> cancelRRTOrder(int? iD) async{ + try { + GenericApiModel? apiResponse; + + Map request = {"Id": iD, "ClickButton": 14}; + + Failure? failure; + await apiClient.post( + body: request, + UPDATE_RRT_ORDER_RC, + isRCService: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + + } + + @override + Future>> getRRTOrders({int? id}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + body: {}, + GET_ALL_RRT_ORDERS_RC, + isRCService: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['response']; + + RRTServiceData serviceData = RRTServiceData(); + list.forEach((item) { + if (item["StatusId"] == 1 || item["StatusId"] == 2) { + // Pending + serviceData.pendingOrders.add(GetCMCAllOrdersResponseModel.fromJson(item)); + } + serviceData.completedOrders.add(GetCMCAllOrdersResponseModel.fromJson(item)); + }); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: serviceData, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getTermsAndCondition() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + body: {}, + GET_USER_TERMS, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final agreement = response['UserAgreementContent']; + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: agreement, + ); + + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + + } + + } diff --git a/lib/features/emergency_services/emergency_services_view_model.dart b/lib/features/emergency_services/emergency_services_view_model.dart index 0b28c2a..400eb04 100644 --- a/lib/features/emergency_services/emergency_services_view_model.dart +++ b/lib/features/emergency_services/emergency_services_view_model.dart @@ -2,39 +2,53 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; - +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_repo.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/PatientERTransportationMethod.dart' show PatientERTransportationMethod; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/ProjectAvgERWaitingTime.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/rrt_procedures_response_model.dart'; import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart' show PlaceDetails; import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_request_type_select.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/terms_and_condition.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/call_ambulance_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/requesting_services_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/tracking_screen.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/nearest_er_page.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart' show AppRoutes; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/model/BottomSheetType.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart'; import 'package:hmg_patient_app_new/widgets/order_tracking/order_tracking_state.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:huawei_map/huawei_map.dart' as HMSCameraServices; @@ -64,8 +78,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { List nearestERList = []; List nearestERFilteredList = []; - List RRTProceduresList = []; - List? hospitalList; List? hmgHospitalList; List? hmcHospitalList; @@ -76,7 +88,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { int hmcCount = 0; bool pickupFromInsideTheLocation = true; List? appointments; - List? orders = []; + List? ambulanceOrders = []; //ambulance selection data section List transportationOptions = []; @@ -84,15 +96,33 @@ class EmergencyServicesViewModel extends ChangeNotifier { AmbulanceCallingPlace callingPlace = AmbulanceCallingPlace.FROM_HOSPITAL; AmbulanceDirection ambulanceDirection = AmbulanceDirection.ONE_WAY; - late RRTProceduresResponseModel selectedRRTProcedure; + bool patientHasAdvanceERBalance = false; + bool isERBookAppointment = false; + late EROnlineCheckInPaymentDetailsResponse erOnlineCheckInPaymentDetailsResponse; BottomSheetType bottomSheetType = BottomSheetType.FIXED; + ///RRT request data + List RRTProceduresList = []; + RRTProceduresResponseModel? selectedRRTProcedure; + bool agreedToTermsAndCondition = false; + RRTServiceData? ordersRRT; + TextEditingController rrtNotes = TextEditingController(); + + List allOrders = []; + List orderDisplayList = []; + bool historyLoading = false; + OrderDislpay currentlyDisplayedOrder = OrderDislpay.ALL; + setSelectedRRTProcedure(RRTProceduresResponseModel procedure) { selectedRRTProcedure = procedure; notifyListeners(); } + get isGMSAvailable { + return appState.isGMSAvailable; + } + EmergencyServicesViewModel({ required this.emergencyServicesRepo, required this.errorHandlerService, @@ -103,9 +133,9 @@ class EmergencyServicesViewModel extends ChangeNotifier { required this.dialogService, }); - get isGMSAvailable - { - return appState.isGMSAvailable; + void setIsERBookAppointment(bool value) { + isERBookAppointment = value; + notifyListeners(); } bool get isArabic => appState.isArabic(); @@ -116,11 +146,24 @@ class EmergencyServicesViewModel extends ChangeNotifier { bool isMyAppointmentsLoading = false; + String? termsAndConditions; + Future getRRTProcedures({Function(dynamic)? onSuccess, Function(String)? onError}) async { + print("the app state is ${appState.isAuthenticated}"); + if (!appState.isAuthenticated) { + dialogService.showErrorBottomSheet( + message: "You Need To Login First To Continue".needTranslation, + onOkPressed: () { + navServices.pop(); + getIt().onLoginPressed(); + }); + return; + } + RRTProceduresList.clear(); notifyListeners(); - final result = await emergencyServicesRepo.getRRTProcedures(); + final result = await emergencyServicesRepo.getRRTProcedures(appState.getLanguageID()); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -157,7 +200,8 @@ class EmergencyServicesViewModel extends ChangeNotifier { if (query.isEmpty) { nearestERFilteredList = nearestERList; } else { - nearestERFilteredList = nearestERList.where((er) => er.projectName != null && er.projectName!.toLowerCase().contains(query.toLowerCase())).toList(); + nearestERFilteredList = + nearestERList.where((er) => er.projectName != null && er.projectName!.toLowerCase().contains(query.toLowerCase())).toList(); } notifyListeners(); } @@ -212,22 +256,19 @@ class EmergencyServicesViewModel extends ChangeNotifier { } GMSMapServices.CameraPosition getGMSLocation() { - return GMSMapServices.CameraPosition( - target: GMSMapServices.LatLng(appState.userLat, appState.userLong), zoom: 18); + return GMSMapServices.CameraPosition(target: GMSMapServices.LatLng(appState.userLat, appState.userLong), zoom: 18); } handleGMSMapCameraMoved(GMSMapServices.CameraPosition value) { - //todo handle the camera moved position for GMS devices + //todo_section handle the camera moved position for GMS devices } HMSCameraServices.CameraPosition getHMSLocation() { - return HMSCameraServices.CameraPosition( - target: HMSCameraServices.LatLng(appState.userLat, appState.userLong),zoom: 18); - + return HMSCameraServices.CameraPosition(target: HMSCameraServices.LatLng(appState.userLat, appState.userLong), zoom: 18); } handleHMSMapCameraMoved(HMSCameraServices.CameraPosition value) { - //todo handle the camera moved position for HMS devices + //todo_section handle the camera moved position for HMS devices } void navigateTOAmbulancePage() { @@ -243,8 +284,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { onSuccess: (position) { updateBottomSheetState(BottomSheetType.FIXED); navServices.push( - CustomPageRoute( - page: CallAmbulancePage(), direction: AxisDirection.down), + CustomPageRoute(page: CallAmbulancePage(), direction: AxisDirection.down), ); }); } else { @@ -252,13 +292,23 @@ class EmergencyServicesViewModel extends ChangeNotifier { message: "You Need To Login First To Continue".needTranslation, onOkPressed: () { navServices.pop(); - navServices.pushAndReplace( - AppRoutes.loginScreen - ); + navServices.pushAndReplace(AppRoutes.loginScreen); }); } } + void navigateToEROnlineCheckIn() { + navServices.push( + CustomPageRoute(page: ErOnlineCheckinHome()), + ); + } + + void navigateToEROnlineCheckInPaymentPage() { + navServices.push( + CustomPageRoute(page: ErOnlineCheckinPaymentDetailsPage()), + ); + } + void updateBottomSheetState(BottomSheetType sheetType) { if (sheetType == BottomSheetType.EXPANDED && selectedHospital == null) { dialogService.showErrorBottomSheet(message: "Kindly Select Hospital".needTranslation); @@ -275,6 +325,160 @@ class EmergencyServicesViewModel extends ChangeNotifier { notifyListeners(); } + Future checkPatientERAdvanceBalance({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await emergencyServicesRepo.checkPatientERAdvanceBalance(); + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) { + patientHasAdvanceERBalance = false; + isERBookAppointment = true; + if (onSuccess != null) { + onSuccess(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + patientHasAdvanceERBalance = false; + isERBookAppointment = true; + } else if (apiResponse.messageStatus == 1) { + patientHasAdvanceERBalance = apiResponse.data; + isERBookAppointment = !patientHasAdvanceERBalance; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getPatientERPaymentInformation({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await emergencyServicesRepo.checkPatientERPaymentInformation(projectID: selectedHospital!.iD); + + result.fold( + (failure) { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + erOnlineCheckInPaymentDetailsResponse = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future ER_CreateAdvancePayment( + {required String paymentMethodName, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await emergencyServicesRepo.createAdvancePaymentForER( + projectID: selectedHospital!.iD, + authUser: appState.getAuthenticatedUser()!, + paymentAmount: erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, + paymentMethodName: paymentMethodName, + paymentReference: paymentReference); + + result.fold( + (failure) { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + // erOnlineCheckInPaymentDetailsResponse = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future addAdvanceNumberRequest( + {required String advanceNumber, + required String paymentReference, + required String appointmentNo, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + final result = await emergencyServicesRepo.addAdvanceNumberRequest( + advanceNumber: advanceNumber, paymentReference: paymentReference, appointmentNo: appointmentNo); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getProjectIDFromNFC({required String nfcCode, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await emergencyServicesRepo.getProjectIDFromNFC(nfcCode: nfcCode); + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + if (onError != null) { + onError(apiResponse.errorMessage!); + } + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future autoGenerateInvoiceERClinic({required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await emergencyServicesRepo.autoGenerateInvoiceERClinic(projectID: projectID); + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + if (onError != null) { + onError(apiResponse.data["InvoiceResponse"]["Message"]); + } + } else if (apiResponse.messageStatus == 1) { + patientHasAdvanceERBalance = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + Future getTransportationOption() async { //handle the cache if the data is present then dont fetch it in the authenticated lifecycle @@ -285,18 +489,13 @@ class EmergencyServicesViewModel extends ChangeNotifier { onOkPressed: () { navServices.pop(); print("inside the ok button"); - navServices.pushAndReplace( - AppRoutes.loginScreen - ); + getIt().onLoginPressed(); }); return; } - if (transportationOptions.isNotEmpty) return; - int? id = appState.getAuthenticatedUser()?.patientId; - LoaderBottomSheet.showLoader( - loadingText: "Getting Ambulance Transport Option".needTranslation); + LoaderBottomSheet.showLoader(loadingText: "Getting Ambulance Transport Option".needTranslation); notifyListeners(); var response = await emergencyServicesRepo.getTransportationMethods(id: id); @@ -315,8 +514,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { Future getTransportationMethods() async { int? id = appState.getAuthenticatedUser()?.patientId; - LoaderBottomSheet.showLoader( - loadingText: "Getting Ambulance Transport Option".needTranslation); + LoaderBottomSheet.showLoader(loadingText: "Getting Ambulance Transport Option".needTranslation); notifyListeners(); var response = await emergencyServicesRepo.getTransportationMethods(id: id); @@ -427,11 +625,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { sourceList = hmcHospitalList; break; } - displayList = sourceList - ?.where((hospital) => - hospital.name != null && - hospital.name!.toLowerCase().contains(query.toLowerCase())) - .toList(); + displayList = sourceList?.where((hospital) => hospital.name != null && hospital.name!.toLowerCase().contains(query.toLowerCase())).toList(); notifyListeners(); } @@ -450,7 +644,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { notifyListeners(); } - void setSelectedHospital(HospitalsModel? hospital) { selectedHospital = hospital; notifyListeners(); @@ -498,13 +691,12 @@ class EmergencyServicesViewModel extends ChangeNotifier { } Future updateAppointment(bool value) async { - if (value) { await getAppointments(); } else { clearAppointmentData(); } - if(appointments?.isNotEmpty == true) { + if (appointments?.isNotEmpty == true) { haveAnAppointment = value; } notifyListeners(); @@ -559,18 +751,24 @@ class EmergencyServicesViewModel extends ChangeNotifier { } Future getTransportationOrders({bool shouldNavigateToTrackingScreen = false, bool showLoader = false}) async { - if(shouldNavigateToTrackingScreen == false && showLoader ) { + if (shouldNavigateToTrackingScreen == false && showLoader) { LoaderBottomSheet.showLoader(loadingText: "Fetching Orders"); } int? id = appState.getAuthenticatedUser()?.patientId; + historyLoading = true; + notifyListeners(); var response = await emergencyServicesRepo.getTransportationOrders(id: id); - if(shouldNavigateToTrackingScreen == false && showLoader ) { - LoaderBottomSheet.hideLoader();} + if (shouldNavigateToTrackingScreen == false && showLoader) { + LoaderBottomSheet.hideLoader(); + } response.fold( (failure) async { + historyLoading = false; + notifyListeners(); if (shouldNavigateToTrackingScreen) { - navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(state: OrderTrackingState.waitingForCall)), ModalRoute.withName("/EmergencyServicesPage")); + navServices.pushAndRemoveUntil( + CustomPageRoute(page: TrackingScreen(state: OrderTrackingState.waitingForCall)), ModalRoute.withName("/EmergencyServicesPage")); } }, (apiResponse) { @@ -583,8 +781,12 @@ class EmergencyServicesViewModel extends ChangeNotifier { )), ModalRoute.withName("/EmergencyServicesPage")); } - - orders = apiResponse.data; + historyLoading = false; + ambulanceOrders = apiResponse.data; + allOrders.clear(); + allOrders.addAll(ambulanceOrders ?? []); + allOrders.addAll(ordersRRT?.completedOrders ?? []); + changeOrderDisplayItems(OrderDislpay.ALL); notifyListeners(); }, ); @@ -658,10 +860,10 @@ class EmergencyServicesViewModel extends ChangeNotifier { Future cancelOrder(AmbulanceRequestOrdersModel? order, {bool shouldPop = false}) async { dialogService.showCommonBottomSheetWithoutH( - message: "Do you want to cancel the order".needTranslation, + message: "Do you want to cancel the request".needTranslation, onOkPressed: () async { navServices.pop(); - LoaderBottomSheet.showLoader(loadingText: "Cancelling Order".needTranslation); + LoaderBottomSheet.showLoader(loadingText: "Cancelling request".needTranslation); var response = await emergencyServicesRepo.cancelOrder(order?.iD, appState.getAuthenticatedUser()?.patientId ?? 0); LoaderBottomSheet.hideLoader(); response.fold((failure) => errorHandlerService.handleError(failure: failure), (success) { @@ -673,4 +875,213 @@ class EmergencyServicesViewModel extends ChangeNotifier { navServices.pop(); }); } + + RRTRequestModel createRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) { + AuthenticatedUser? user = appState.getAuthenticatedUser(); + if (user == null) throw Exception("Authentication Required to Continue"); + + RRTRequestModel rrtRequestModel = new RRTRequestModel(); + Procedures procedures = new Procedures(); + rrtRequestModel.procedures = []; + + procedures.serviceID = selectedRRTProcedure?.procedureID; + + rrtRequestModel.latitude = ((result?.geometry.location.lat) ?? place?.lat); + rrtRequestModel.longitude = ((result?.geometry.location.lat) ?? place?.lat); + rrtRequestModel.additionalDetails = ""; + rrtRequestModel.nationality = user.nationalityId; + rrtRequestModel.paymentAmount = selectedRRTProcedure?.patientShareWithTax; + rrtRequestModel.nearestProjectId = 0; + rrtRequestModel.patientId = user.patientId; + rrtRequestModel.patientOutSa = user.outSa; + rrtRequestModel.procedures!.add(procedures); + + return rrtRequestModel; + } + + ///method to toggle the value for the aggremnent to the terms and conditon for the rrt + void setTermsAndConditions(bool value) { + agreedToTermsAndCondition = value; + notifyListeners(); + } + + FutureOr submitRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) async { + RRTRequestModel request = createRRTRequest(result, place, placePrediction); + navServices.push(CustomPageRoute(page: RequestingServicesPage())); + + var response = await emergencyServicesRepo.submitRRTRequest(request); + response.fold((failure) { + navServices.pushAndRemoveUntil( + CustomPageRoute( + page: TrackingScreen( + isRRTOrder: true, + state: OrderTrackingState.failed, + )), + ModalRoute.withName("/EmergencyServicesPage")); + }, (success) { + getRRTOrders(shouldNavigateToTrackingScreen: true); + }); + } + + Future getRRTOrders({bool shouldNavigateToTrackingScreen = false, bool showLoader = false}) async { + if (shouldNavigateToTrackingScreen == false && showLoader) { + LoaderBottomSheet.showLoader(loadingText: "Fetching Orders"); + } + historyLoading = true; + notifyListeners(); + int? id = appState.getAuthenticatedUser()?.patientId; + + var response = await emergencyServicesRepo.getRRTOrders(id: id); + if (shouldNavigateToTrackingScreen == false && showLoader) { + LoaderBottomSheet.hideLoader(); + } + response.fold( + (failure) async { + historyLoading = false; + notifyListeners(); + if (shouldNavigateToTrackingScreen) { + navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(isRRTOrder: true, state: OrderTrackingState.waitingForCall)), + ModalRoute.withName("/EmergencyServicesPage")); + } + }, + (apiResponse) { + if (shouldNavigateToTrackingScreen) { + navServices.pushAndRemoveUntil( + CustomPageRoute( + page: TrackingScreen( + state: OrderTrackingState.waitingForCall, + isRRTOrder: true, + rrtOrder: apiResponse.data?.pendingOrders.first, + )), + ModalRoute.withName("/EmergencyServicesPage")); + } + historyLoading = false; + ordersRRT = apiResponse.data; + allOrders.clear(); + allOrders.addAll(ambulanceOrders ?? []); + allOrders.addAll(ordersRRT?.completedOrders ?? []); + changeOrderDisplayItems(OrderDislpay.ALL); + notifyListeners(); + }, + ); + } + + FutureOr cancelRRTOrder(int? orderID, {bool shouldPop = false}) async { + dialogService.showCommonBottomSheetWithoutH( + message: "Do you want to cancel the request".needTranslation, + onOkPressed: () async { + navServices.pop(); + LoaderBottomSheet.showLoader(loadingText: "Cancelling request".needTranslation); + var response = await emergencyServicesRepo.cancelRRTOrder(orderID); + LoaderBottomSheet.hideLoader(); + response.fold((failure) => errorHandlerService.handleError(failure: failure), (success) { + getRRTOrders(); + if (shouldPop) navServices.pop(); + }); + }, + onCancelPressed: () { + navServices.pop(); + }); + } + + void changeOrderDisplayItems(OrderDislpay currentlyDisplayedOrder) { + this.currentlyDisplayedOrder = currentlyDisplayedOrder; + switch (currentlyDisplayedOrder) { + case OrderDislpay.ALL: + orderDisplayList = allOrders; + break; + case OrderDislpay.RRT: + orderDisplayList = ordersRRT?.completedOrders ?? []; + break; + case OrderDislpay.AMBULANCE: + orderDisplayList = ambulanceOrders ?? []; + break; + } + notifyListeners(); + } + + void openRRT() { + print("the app state is ${appState.isAuthenticated}"); + if (appState.isAuthenticated) { + if (agreedToTermsAndCondition == false) { + dialogService.showErrorBottomSheet( + message: "You Need To Agree To Terms And Conditions".needTranslation, + onOkPressed: () { + if (navServices.context == null) return; + showCommonBottomSheetWithoutHeight( + navServices.context!, + padding: EdgeInsets.only(top: 24.h), + titleWidget: Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + iconColor: Color(0xff2B353E), + fit: BoxFit.contain, + ), + ).onPress(() { + navServices.pop(); + }), + // title: "Rapid Response Team (RRT)".needTranslation, + child: RrtRequestTypeSelect(), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () { + navServices.pop(); + }, + ); + }); + return; + } + placeValueInController(); + locationUtils!.getLocation( + isShowConfirmDialog: true, + onSuccess: (position) async { + updateBottomSheetState(BottomSheetType.FIXED); + bool result = await navServices.push( + CustomPageRoute( + page: MapUtilityScreen( + confirmButtonString: "Submit Request".needTranslation, + titleString: "Select Location".needTranslation, + subTitleString: "Please select the location".needTranslation, + isGmsAvailable: appState.isGMSAvailable, + ), + direction: AxisDirection.down), + ); + if (result) { + LocationViewModel locationViewModel = getIt.get(); + GeocodeResponse? response = locationViewModel.geocodeResponse; + PlaceDetails? placeDetails = locationViewModel.placeDetails; + PlacePrediction? placePrediction = locationViewModel.selectedPrediction; + submitRRTRequest(response?.results.first, placeDetails, placePrediction); + } + }); + } else { + dialogService.showErrorBottomSheet( + message: "You Need To Login First To Continue".needTranslation, + onOkPressed: () { + navServices.pop(); + getIt().onLoginPressed(); + }); + } + } + + clearRRTData() { + selectedRRTProcedure = null; + } + + FutureOr getTermsAndConditions() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Terms And Conditions".needTranslation); + var response = await emergencyServicesRepo.getTermsAndCondition(); + LoaderBottomSheet.hideLoader(); + response.fold((failure) => errorHandlerService.handleError(failure: failure), (success) { + termsAndConditions = success.data; + print("the response terms are $termsAndConditions"); + notifyListeners(); + navServices.push( + CustomPageRoute(page: TermsAndCondition(termsAndCondition: success.data ?? ""), direction: AxisDirection.down), + ); + }); + } } diff --git a/lib/features/emergency_services/models/OrderDisplay.dart b/lib/features/emergency_services/models/OrderDisplay.dart new file mode 100644 index 0000000..9f1e929 --- /dev/null +++ b/lib/features/emergency_services/models/OrderDisplay.dart @@ -0,0 +1,3 @@ +enum OrderDislpay{ + ALL,RRT,AMBULANCE +} \ No newline at end of file diff --git a/lib/features/emergency_services/models/request_model/RRTRequestModel.dart b/lib/features/emergency_services/models/request_model/RRTRequestModel.dart new file mode 100644 index 0000000..dfb5b1b --- /dev/null +++ b/lib/features/emergency_services/models/request_model/RRTRequestModel.dart @@ -0,0 +1,75 @@ +class RRTRequestModel { + num? patientId; + int? patientOutSa; + bool? isOutPatient; + int? nearestProjectId; + num? longitude; + num? latitude; + String? additionalDetails; + String? nationality; + num? paymentAmount; + List? procedures; + + RRTRequestModel( + {this.patientId, + this.patientOutSa, + this.isOutPatient, + this.nearestProjectId, + this.longitude, + this.latitude, + this.additionalDetails, + this.nationality, + this.paymentAmount, + this.procedures}); + + RRTRequestModel.fromJson(Map json) { + patientId = json['patientId']; + patientOutSa = json['patientOutSa']; + isOutPatient = json['isOutPatient']; + nearestProjectId = json['nearestProjectId']; + longitude = json['longitude']; + latitude = json['latitude']; + additionalDetails = json['additionalDetails']; + nationality = json['nationality']; + paymentAmount = json['paymentAmount']; + if (json['procedures'] != null) { + procedures = []; + json['procedures'].forEach((v) { + procedures!.add(new Procedures.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['patientId'] = this.patientId; + data['patientOutSa'] = this.patientOutSa; + data['isOutPatient'] = this.isOutPatient; + data['nearestProjectId'] = this.nearestProjectId; + data['longitude'] = this.longitude; + data['latitude'] = this.latitude; + data['additionalDetails'] = this.additionalDetails; + data['nationality'] = this.nationality; + data['paymentAmount'] = this.paymentAmount; + if (this.procedures != null) { + data['procedures'] = this.procedures!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Procedures { + String? serviceID; + + Procedures({this.serviceID}); + + Procedures.fromJson(Map json) { + serviceID = json['ServiceID']; + } + + Map toJson() { + final Map data = new Map(); + data['ServiceID'] = this.serviceID; + return data; + } +} diff --git a/lib/features/emergency_services/models/request_model/service_price.dart b/lib/features/emergency_services/models/request_model/service_price.dart new file mode 100644 index 0000000..abb57be --- /dev/null +++ b/lib/features/emergency_services/models/request_model/service_price.dart @@ -0,0 +1,53 @@ +class ServicePrice { + String? currency; + dynamic maxPrice; + dynamic maxTotalPrice; + dynamic maxVAT; + dynamic minPrice; + dynamic minTotalPrice; + dynamic minVAT; + dynamic price; + dynamic totalPrice; + dynamic vat; + + ServicePrice({ + this.currency, + this.maxPrice, + this.maxTotalPrice, + this.maxVAT, + this.minPrice, + this.minTotalPrice, + this.minVAT, + this.price, + this.totalPrice, + this.vat}); + + ServicePrice.fromJson(dynamic json) { + currency = json["Currency"]; + maxPrice = json["MaxPrice"]; + maxTotalPrice = json["MaxTotalPrice"]; + maxVAT = json["MaxVAT"]; + minPrice = json["MinPrice"]; + minTotalPrice = json["MinTotalPrice"]; + minVAT = json["MinVAT"]; + price = json["Price"]; + totalPrice = json["TotalPrice"]; + vat = json["VAT"]; + } + + Map toJson() { + var map = {}; + map["Currency"] = currency; + map["MaxPrice"] = maxPrice; + map["MaxTotalPrice"] = maxTotalPrice; + map["MaxVAT"] = maxVAT; + map["MinPrice"] = minPrice; + map["MinTotalPrice"] = minTotalPrice; + map["MinVAT"] = minVAT; + map["Price"] = price; + map["TotalPrice"] = totalPrice; + map["VAT"] = vat; + return map; + } + +} \ No newline at end of file diff --git a/lib/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart b/lib/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart new file mode 100644 index 0000000..5cac2cc --- /dev/null +++ b/lib/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart @@ -0,0 +1,108 @@ +class EROnlineCheckInPaymentDetailsResponse { + num? cashPrice; + num? cashPriceTax; + num? cashPriceWithTax; + int? companyId; + String? companyName; + num? companyShareWithTax; + dynamic errCode; + int? groupID; + String? insurancePolicyNo; + String? message; + String? patientCardID; + num? patientShare; + num? patientShareWithTax; + num? patientTaxAmount; + int? policyId; + String? policyName; + String? procedureId; + String? procedureName; + dynamic setupID; + int? statusCode; + String? subPolicyNo; + bool? isCash; + bool? isEligible; + bool? isInsured; + + EROnlineCheckInPaymentDetailsResponse( + {this.cashPrice, + this.cashPriceTax, + this.cashPriceWithTax, + this.companyId, + this.companyName, + this.companyShareWithTax, + this.errCode, + this.groupID, + this.insurancePolicyNo, + this.message, + this.patientCardID, + this.patientShare, + this.patientShareWithTax, + this.patientTaxAmount, + this.policyId, + this.policyName, + this.procedureId, + this.procedureName, + this.setupID, + this.statusCode, + this.subPolicyNo, + this.isCash, + this.isEligible, + this.isInsured}); + + EROnlineCheckInPaymentDetailsResponse.fromJson(Map json) { + cashPrice = json['CashPrice']; + cashPriceTax = json['CashPriceTax']; + cashPriceWithTax = json['CashPriceWithTax']; + companyId = json['CompanyId']; + companyName = json['CompanyName']; + companyShareWithTax = json['CompanyShareWithTax']; + errCode = json['ErrCode']; + groupID = json['GroupID']; + insurancePolicyNo = json['InsurancePolicyNo']; + message = json['Message']; + patientCardID = json['PatientCardID']; + patientShare = json['PatientShare']; + patientShareWithTax = json['PatientShareWithTax']; + patientTaxAmount = json['PatientTaxAmount']; + policyId = json['PolicyId']; + policyName = json['PolicyName']; + procedureId = json['ProcedureId']; + procedureName = json['ProcedureName']; + setupID = json['SetupID']; + statusCode = json['StatusCode']; + subPolicyNo = json['SubPolicyNo']; + isCash = json['IsCash']; + isEligible = json['IsEligible']; + isInsured = json['IsInsured']; + } + + Map toJson() { + final Map data = new Map(); + data['CashPrice'] = this.cashPrice; + data['CashPriceTax'] = this.cashPriceTax; + data['CashPriceWithTax'] = this.cashPriceWithTax; + data['CompanyId'] = this.companyId; + data['CompanyName'] = this.companyName; + data['CompanyShareWithTax'] = this.companyShareWithTax; + data['ErrCode'] = this.errCode; + data['GroupID'] = this.groupID; + data['InsurancePolicyNo'] = this.insurancePolicyNo; + data['Message'] = this.message; + data['PatientCardID'] = this.patientCardID; + data['PatientShare'] = this.patientShare; + data['PatientShareWithTax'] = this.patientShareWithTax; + data['PatientTaxAmount'] = this.patientTaxAmount; + data['PolicyId'] = this.policyId; + data['PolicyName'] = this.policyName; + data['ProcedureId'] = this.procedureId; + data['ProcedureName'] = this.procedureName; + data['SetupID'] = this.setupID; + data['StatusCode'] = this.statusCode; + data['SubPolicyNo'] = this.subPolicyNo; + data['IsCash'] = this.isCash; + data['IsEligible'] = this.isEligible; + data['IsInsured'] = this.isInsured; + return data; + } +} diff --git a/lib/features/emergency_services/models/resp_model/RRTServiceData.dart b/lib/features/emergency_services/models/resp_model/RRTServiceData.dart new file mode 100644 index 0000000..e4e7ce7 --- /dev/null +++ b/lib/features/emergency_services/models/resp_model/RRTServiceData.dart @@ -0,0 +1,406 @@ +class RRTServiceData { + List pendingOrders = []; + List completedOrders = []; + ServicePrice servicePrice = ServicePrice(); +} + +class GetCMCAllOrdersResponseModel { + int? iD; + int? patientId; + int? patientOutSa; + bool? isOutPatient; + int? projectId; + int? nearestProjectId; + dynamic longitude; + dynamic latitude; + dynamic appointmentNo; + dynamic dischargeId; + int? statusId; + int? serviceId; + int? channel; + Orderpayment? orderpayment; + dynamic orderselectedservice; + dynamic wforder; + dynamic orderapprovalobj; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + String? statusText; + int? paymentStatus; + dynamic clientRequestid; + dynamic paymentStatusText; + String? projectName; + String? nearestProjectName; + dynamic paymentAmount; + WFOrder? wFOrder; + String? serviceText; + bool? isSentForApproval; + int? exaCartOrderId; + bool? isTimer; + int? timeSeconds; + int? totalPendingSeconds; + int? timeMinute; + int? timeHour; + int? timeTotalSeconds; + int? timeTotalMinute; + int? timeTotalHour; + dynamic approvalStatus; + bool? isActive; + int? clickButton; + List? procedures; + dynamic pickupLocation; + dynamic dropOffLocation; + dynamic clinicName; + dynamic doctorName; + dynamic branch; + dynamic time; + dynamic notes; + + GetCMCAllOrdersResponseModel( + {this.iD, + this.patientId, + this.patientOutSa, + this.isOutPatient, + this.projectId, + this.nearestProjectId, + this.longitude, + this.latitude, + this.appointmentNo, + this.dischargeId, + this.statusId, + this.serviceId, + this.channel, + this.orderpayment, + this.orderselectedservice, + this.wforder, + this.orderapprovalobj, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted, + this.statusText, + this.paymentStatus, + this.clientRequestid, + this.paymentStatusText, + this.projectName, + this.nearestProjectName, + this.paymentAmount, + this.wFOrder, + this.serviceText, + this.isSentForApproval, + this.exaCartOrderId, + this.isTimer, + this.timeSeconds, + this.totalPendingSeconds, + this.timeMinute, + this.timeHour, + this.timeTotalSeconds, + this.timeTotalMinute, + this.timeTotalHour, + this.approvalStatus, + this.isActive, + this.clickButton, + this.procedures, + this.pickupLocation, + this.dropOffLocation, + this.clinicName, + this.doctorName, + this.branch, + this.time, + this.notes}); + + GetCMCAllOrdersResponseModel.fromJson(Map json) { + iD = json['ID']; + patientId = json['PatientId']; + patientOutSa = json['PatientOutSa']; + isOutPatient = json['IsOutPatient']; + projectId = json['ProjectId']; + nearestProjectId = json['NearestProjectId']; + longitude = json['Longitude']; + latitude = json['Latitude']; + appointmentNo = json['AppointmentNo']; + dischargeId = json['DischargeId']; + statusId = json['StatusId']; + serviceId = json['ServiceId']; + channel = json['Channel']; + orderpayment = json['orderpayment'] != null + ? new Orderpayment.fromJson(json['orderpayment']) + : null; + orderselectedservice = json['orderselectedservice']; + wforder = json['wforder']; + orderapprovalobj = json['orderapprovalobj']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + statusText = json['StatusText']; + paymentStatus = json['PaymentStatus']; + clientRequestid = json['ClientRequestid']; + paymentStatusText = json['PaymentStatusText']; + projectName = json['ProjectName']; + nearestProjectName = json['NearestProjectName']; + paymentAmount = json['PaymentAmount']; + wFOrder = json['WF_order'] != null + ? new WFOrder.fromJson(json['WF_order']) + : null; + serviceText = json['ServiceText']; + isSentForApproval = json['isSentForApproval']; + exaCartOrderId = json['ExaCart_OrderId']; + isTimer = json['isTimer']; + timeSeconds = json['TimeSeconds']; + totalPendingSeconds = json['TotalPendingSeconds']; + timeMinute = json['TimeMinute']; + timeHour = json['TimeHour']; + timeTotalSeconds = json['TimeTotalSeconds']; + timeTotalMinute = json['TimeTotalMinute']; + timeTotalHour = json['TimeTotalHour']; + approvalStatus = json['ApprovalStatus']; + isActive = json['isActive']; + clickButton = json['ClickButton']; + pickupLocation = json['PickupLocation']; + dropOffLocation = json['DropOffLocation']; + clinicName = json['clinicName']; + doctorName = json['DoctorName']; + branch = json['Branch']; + time = json['Time']; + notes = json['Notes']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['PatientId'] = this.patientId; + data['PatientOutSa'] = this.patientOutSa; + data['IsOutPatient'] = this.isOutPatient; + data['ProjectId'] = this.projectId; + data['NearestProjectId'] = this.nearestProjectId; + data['Longitude'] = this.longitude; + data['Latitude'] = this.latitude; + data['AppointmentNo'] = this.appointmentNo; + data['DischargeId'] = this.dischargeId; + data['StatusId'] = this.statusId; + data['ServiceId'] = this.serviceId; + data['Channel'] = this.channel; + if (this.orderpayment != null) { + data['orderpayment'] = this.orderpayment!.toJson(); + } + data['orderselectedservice'] = this.orderselectedservice; + + data['wforder'] = this.wforder; + data['orderapprovalobj'] = this.orderapprovalobj; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + data['StatusText'] = this.statusText; + data['PaymentStatus'] = this.paymentStatus; + data['ClientRequestid'] = this.clientRequestid; + data['PaymentStatusText'] = this.paymentStatusText; + data['ProjectName'] = this.projectName; + data['NearestProjectName'] = this.nearestProjectName; + data['PaymentAmount'] = this.paymentAmount; + if (this.wFOrder != null) { + data['WF_order'] = this.wFOrder!.toJson(); + } + data['ServiceText'] = this.serviceText; + data['isSentForApproval'] = this.isSentForApproval; + data['ExaCart_OrderId'] = this.exaCartOrderId; + data['isTimer'] = this.isTimer; + data['TimeSeconds'] = this.timeSeconds; + data['TotalPendingSeconds'] = this.totalPendingSeconds; + data['TimeMinute'] = this.timeMinute; + data['TimeHour'] = this.timeHour; + data['TimeTotalSeconds'] = this.timeTotalSeconds; + data['TimeTotalMinute'] = this.timeTotalMinute; + data['TimeTotalHour'] = this.timeTotalHour; + data['ApprovalStatus'] = this.approvalStatus; + data['isActive'] = this.isActive; + data['ClickButton'] = this.clickButton; + data['PickupLocation'] = this.pickupLocation; + data['DropOffLocation'] = this.dropOffLocation; + data['clinicName'] = this.clinicName; + data['DoctorName'] = this.doctorName; + data['Branch'] = this.branch; + data['Time'] = this.time; + data['Notes'] = this.notes; + return data; + } +} + +class Orderpayment { + int? iD; + int? orderId; + dynamic clientRequestId; + dynamic totalAmount; + int? paymentStatus; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + Orderpayment( + {this.iD, + this.orderId, + this.clientRequestId, + this.totalAmount, + this.paymentStatus, + this.order, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted}); + + Orderpayment.fromJson(Map json) { + iD = json['ID']; + orderId = json['OrderId']; + clientRequestId = json['ClientRequestId']; + totalAmount = json['TotalAmount']; + paymentStatus = json['PaymentStatus']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['OrderId'] = this.orderId; + data['ClientRequestId'] = this.clientRequestId; + data['TotalAmount'] = this.totalAmount; + data['PaymentStatus'] = this.paymentStatus; + data['Order'] = this.order; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + return data; + } +} + +class WFOrder { + dynamic wfButtonsDTO; + int? iD; + int? orderId; + int? previousStep; + int? nextStep; + int? serviceId; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + WFOrder( + {this.wfButtonsDTO, + this.iD, + this.orderId, + this.previousStep, + this.nextStep, + this.serviceId, + this.order, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted}); + + WFOrder.fromJson(Map json) { + wfButtonsDTO = json['wf_ButtonsDTO']; + iD = json['ID']; + orderId = json['OrderId']; + previousStep = json['PreviousStep']; + nextStep = json['NextStep']; + serviceId = json['ServiceId']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = new Map(); + data['wf_ButtonsDTO'] = this.wfButtonsDTO; + data['ID'] = this.iD; + data['OrderId'] = this.orderId; + data['PreviousStep'] = this.previousStep; + data['NextStep'] = this.nextStep; + data['ServiceId'] = this.serviceId; + data['Order'] = this.order; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + return data; + } +} + + +class ServicePrice { + String? currency; + dynamic maxPrice; + dynamic maxTotalPrice; + dynamic maxVAT; + dynamic minPrice; + dynamic minTotalPrice; + dynamic minVAT; + dynamic price; + dynamic totalPrice; + dynamic vat; + + ServicePrice({ + this.currency, + this.maxPrice, + this.maxTotalPrice, + this.maxVAT, + this.minPrice, + this.minTotalPrice, + this.minVAT, + this.price, + this.totalPrice, + this.vat}); + + ServicePrice.fromJson(dynamic json) { + currency = json["Currency"]; + maxPrice = json["MaxPrice"]; + maxTotalPrice = json["MaxTotalPrice"]; + maxVAT = json["MaxVAT"]; + minPrice = json["MinPrice"]; + minTotalPrice = json["MinTotalPrice"]; + minVAT = json["MinVAT"]; + price = json["Price"]; + totalPrice = json["TotalPrice"]; + vat = json["VAT"]; + } + + Map toJson() { + var map = {}; + map["Currency"] = currency; + map["MaxPrice"] = maxPrice; + map["MaxTotalPrice"] = maxTotalPrice; + map["MaxVAT"] = maxVAT; + map["MinPrice"] = minPrice; + map["MinTotalPrice"] = minTotalPrice; + map["MinVAT"] = minVAT; + map["Price"] = price; + map["TotalPrice"] = totalPrice; + map["VAT"] = vat; + return map; + } + +} \ No newline at end of file diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart new file mode 100644 index 0000000..85e6018 --- /dev/null +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -0,0 +1,971 @@ +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/check_activation_e_referral_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/order_update_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/search_e_referral_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +import 'models/req_models/create_e_referral_model.dart'; +import 'models/req_models/send_activation_code_ereferral_req_model.dart'; +import 'models/resq_models/covid_get_test_proceedure_resp.dart'; +import 'models/resq_models/get_covid_payment_info_resp.dart'; +import 'models/resq_models/relationship_type_resp_mode.dart'; +import 'models/resq_models/search_e_referral_resp_model.dart'; +import 'models/resq_models/vital_sign_respo_model.dart'; + +abstract class HmgServicesRepo { + Future>>> getAllComprehensiveCheckupOrders(); + + Future>>> getAllHomeHealthCareCheckupOrders(); + + Future>> updateCmcPresOrder(OrderUpdateRequestModel requestModel); + + Future>> updateHhcPresOrder(OrderUpdateRequestModel requestModel); + + Future>>> getAllCmcServices({required int patientID}); + + Future>>> getAllHhcServices({required int patientID}); + + Future>>> getHospitalsList(); + + Future>> addCmcOrder({ + required int projectID, + required int orderServiceID, + required List services, + }); + + Future>> addHhcOrder({ + required int projectID, + required int orderServiceID, + required List services, + }); + + Future>>> getRelationshipTypes(); + + Future>>> getAllCities(); + + Future>> sendEReferralActivationCode(SendActivationCodeForEReferralRequestModel requestModel); + + Future>> checkEReferralActivationCode(CheckActivationCodeForEReferralRequestModel requestModel); + + Future>> createEReferral(CreateEReferralRequestModel requestModel); + + + Future>>> searchEReferral(SearchEReferralRequestModel requestModel); + + Future>>> getCovidTestProcedures(); + + Future>> getCovidPaymentInfo(String procedureID, int projectID); + + Future>>> getPatientVitalSign(); +} + +class HmgServicesRepoImp implements HmgServicesRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + HmgServicesRepoImp({required this.apiClient, required this.loggerService}); + + @override + Future>>> getAllComprehensiveCheckupOrders() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.allCMCOrdersRc, + isRCService: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("CMC Orders API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List cmcOrdersList = []; + // Log the full response for debugging + // Extract MessageStatus and ErrorEndUserMessage from root level + final apiErrorMessage = response['ErrorEndUserMessage'] as String?; + // Parse the response array + if (response['response'] != null && response['response'] is List) { + final ordersList = response['response'] as List; + + for (var orderJson in ordersList) { + if (orderJson is Map) { + try { + cmcOrdersList.add(GetCMCAllOrdersResponseModel.fromJson(orderJson)); + } catch (e) { + loggerService.logError("Error parsing individual order: ${e.toString()}"); + } + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: apiErrorMessage ?? errorMessage, + data: cmcOrdersList, + ); + } catch (e) { + loggerService.logError("Error parsing CMC orders: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in getAllCmcOrders: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getAllHomeHealthCareCheckupOrders() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.allHHCOrdersRc, + isRCService: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("HHC Orders API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List cmcOrdersList = []; + // Log the full response for debugging + // Extract MessageStatus and ErrorEndUserMessage from root level + final apiErrorMessage = response['ErrorEndUserMessage'] as String?; + // Parse the response array + if (response['response'] != null && response['response'] is List) { + final ordersList = response['response'] as List; + + for (var orderJson in ordersList) { + if (orderJson is Map) { + try { + cmcOrdersList.add(GetCMCAllOrdersResponseModel.fromJson(orderJson)); + } catch (e) { + loggerService.logError("Error parsing individual order: ${e.toString()}"); + } + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: apiErrorMessage ?? errorMessage, + data: cmcOrdersList, + ); + } catch (e) { + loggerService.logError("Error parsing HHC orders: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in getAllHHCOrders: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getAllCmcServices({required int patientID}) async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + '${ApiConsts.allCMCServicesRc}?patientID=$patientID', + isRCService: true, + isAllowAny: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("CMC Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List cmcServicesList = []; + + if (response['response'] != null && response['response'] is List) { + final servicesList = response['response'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + cmcServicesList.add(GetCMCServicesResponseModel.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: cmcServicesList, + ); + } catch (e) { + loggerService.logError("Error parsing CMC services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getAllHhcServices({required int patientID}) async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + '${ApiConsts.allHHCServicesRc}?patientID=$patientID', + isRCService: true, + isAllowAny: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("HHC Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List hhcServicesList = []; + + if (response['response'] != null && response['response'] is List) { + final servicesList = response['response'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + hhcServicesList.add(GetCMCServicesResponseModel.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: hhcServicesList, + ); + } catch (e) { + loggerService.logError("Error parsing HHC services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllHhcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateCmcPresOrder(OrderUpdateRequestModel requestModel) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.updateCMCOrder, + isRCService: true, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Update CMC Order API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: true, + ); + + loggerService.logInfo("CMC Order updated successfully: PresOrderID=${requestModel.presOrderID}"); + } catch (e) { + loggerService.logError("Error processing update CMC order response: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in updateCmcPresOrder: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateHhcPresOrder(OrderUpdateRequestModel requestModel) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.updateHHCOrder, + isRCService: true, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Update HHC Order API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: true, + ); + + loggerService.logInfo("HHC Order updated successfully: PresOrderID=${requestModel.presOrderID}"); + } catch (e) { + loggerService.logError("Error processing update HHC order response: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in updateHhcPresOrder: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getHospitalsList() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getHospitalsList, + isRCService: false, // This uses the base HIS API URL, not RC + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Get Hospitals List API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List hospitalsList = []; + + loggerService.logInfo("Hospitals List Raw Response: $response"); + + if (response['ListProject'] != null && response['ListProject'] is List) { + final projectsList = response['ListProject'] as List; + + for (var projectJson in projectsList) { + try { + if (projectJson is Map) { + hospitalsList.add(HospitalsModel.fromJson(projectJson)); + } + } catch (e) { + loggerService.logError('Error parsing hospital item: ${e.toString()}'); + } + } + } else { + loggerService.logInfo('Hospitals list response array is empty or missing'); + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: hospitalsList, + ); + + loggerService.logInfo("Hospitals fetched successfully: ${hospitalsList.length} hospitals"); + } catch (e) { + loggerService.logError("Error parsing hospitals list: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in getHospitalsList: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addCmcOrder({ + required int projectID, + required int orderServiceID, + required List services, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + final requestBody = { + 'ProjectID': projectID, + 'OrderServiceID': orderServiceID, + 'procedures': services.map((service) => service.toJson()).toList(), + }; + + await apiClient.post( + ApiConsts.addCMCOrder, + isRCService: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Add CMC Order API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + int requestId = 0; + if (response is Map) { + requestId = response['response']; + } + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: requestId, + ); + + loggerService.logInfo("CMC Order added successfully: ProjectID=$projectID, OrderServiceID=$orderServiceID"); + } catch (e) { + loggerService.logError("Error processing add CMC order response: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in addCmcOrder: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addHhcOrder({ + required int projectID, + required int orderServiceID, + required List services, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + final requestBody = { + 'ProjectID': projectID, + 'OrderServiceID': orderServiceID, + 'procedures': services.map((service) => service.toJson()).toList(), + }; + + await apiClient.post( + ApiConsts.addHHCOrder, + isRCService: true, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Add HHC Order API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + int requestId = 0; + if (response is Map) { + requestId = response['response']; + } + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: requestId, + ); + + loggerService.logInfo("HHC Order added successfully: ProjectID=$projectID, OrderServiceID=$orderServiceID"); + } catch (e) { + loggerService.logError("Error processing add HHC order response: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in addHhcOrder: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + + /*e-referral functions*/ + + + @override + Future>>> getRelationshipTypes() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getAllRelationshipTypes, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List relationshipList = []; + + if (response['List_EReferralResult'] != null && response['List_EReferralResult'] is List) { + final servicesList = response['List_EReferralResult'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + relationshipList.add(GetAllRelationshipTypeResponseModel.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: relationshipList, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getAllCities() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getAllCities, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List citiesList = []; + + if (response['ListCities'] != null && response['ListCities'] is List) { + final servicesList = response['ListCities'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + citiesList.add(GetAllCitiesResponseModel.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: citiesList, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>> sendEReferralActivationCode(SendActivationCodeForEReferralRequestModel requestModel) async { + GenericApiModel? apiResponse; + try { + + Failure? failure; + await apiClient.post( + ApiConsts.sendActivationCodeForEReferral, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response['LogInTokenID'] as String, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> createEReferral(CreateEReferralRequestModel requestModel) async { + GenericApiModel? apiResponse; + try { + Failure? failure; + await apiClient.post( + ApiConsts.createEReferral, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response['ReferralNumber'].toString(), + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>> checkEReferralActivationCode(CheckActivationCodeForEReferralRequestModel requestModel) async { + GenericApiModel? apiResponse; + try { + Failure? failure; + await apiClient.post( + ApiConsts.checkActivationCodeForEReferral, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response['IsAuthenticated'].toString(), + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getAllCmcServices: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + + + @override + Future>>> searchEReferral(SearchEReferralRequestModel requestBody) async { + + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getEReferrals, + body: requestBody.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("EReferral Services API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List searchReferral = []; + + if (response['List_EReferrals'] != null && response['List_EReferrals'] is List) { + final servicesList = response['List_EReferrals'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + searchReferral.add(SearchEReferralResponseModel.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: searchReferral, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in Search Referral: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>>> getCovidTestProcedures() async { + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + GET_COVID_DRIVETHRU_PROCEDURES_LIST, + body: {"TestTypeEnum":2,"TestProcedureEnum":3,}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Covid Test Procedure : $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List covidTestProcedure = []; + + if (response['COVID19_TestProceduresList'] != null && response['COVID19_TestProceduresList'] is List) { + final servicesList = response['COVID19_TestProceduresList'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + covidTestProcedure.add(Covid19GetTestProceduresResp.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: covidTestProcedure, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in Search Referral: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getCovidPaymentInfo(String procedureID, int projectID) async { + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + GET_COVID_DRIVETHRU_PAYMENT_INFO, + body: {"TestTypeEnum":2,"TestProcedureEnum":3, "ProcedureId":procedureID, "ProjectID":projectID,}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Covid Test Procedure : $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + Covid19GetPaymentInfo covidPaymentInfo = Covid19GetPaymentInfo.fromJson(response["COVID19_PatientShare"]); + + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: covidPaymentInfo, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in Search Referral: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getPatientVitalSign() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + GET_PATIENT_VITAL_SIGN, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Patient Vital Sign API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List vitalSignList = []; + + if (response['List_DoctorPatientVitalSign'] != null && response['List_DoctorPatientVitalSign'] is List) { + final vitalSignsList = response['List_DoctorPatientVitalSign'] as List; + + for (var vitalSignJson in vitalSignsList) { + if (vitalSignJson is Map) { + vitalSignList.add(VitalSignResModel.fromJson(vitalSignJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: vitalSignList, + ); + } catch (e) { + loggerService.logError("Error parsing Patient Vital Sign: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getPatientVitalSign: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + +} diff --git a/lib/features/hmg_services/hmg_services_view_model.dart b/lib/features/hmg_services/hmg_services_view_model.dart new file mode 100644 index 0000000..c55a11c --- /dev/null +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -0,0 +1,918 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/order_update_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/search_e_referral_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; + +import 'models/req_models/check_activation_e_referral_req_model.dart'; +import 'models/resq_models/get_covid_payment_info_resp.dart'; +import 'models/resq_models/relationship_type_resp_mode.dart'; +import 'models/ui_models/covid_questionnare_model.dart'; + +class HmgServicesViewModel extends ChangeNotifier { + final HmgServicesRepo hmgServicesRepo; + final BookAppointmentsRepo bookAppointmentsRepo; + final ErrorHandlerService errorHandlerService; + final NavigationService navigationService; + + HmgServicesViewModel( + {required this.bookAppointmentsRepo, required this.hmgServicesRepo, required this.errorHandlerService, required this.navigationService}); + + bool isCmcOrdersLoading = false; + bool isCmcServicesLoading = false; + bool isUpdatingOrder = false; + bool isHospitalListLoading = false; + bool isVitalSignLoading = false; + + // HHC specific loading states + bool isHhcOrdersLoading = false; + bool isHhcServicesLoading = false; + + List cmcOrdersList = []; + List cmcServicesList = []; + List hospitalsList = []; + List filteredHospitalsList = []; + HospitalsModel? selectedHospital; + List vitalSignList = []; + + // Vital Sign PageView Controller + PageController _vitalSignPageController = PageController(); + PageController get vitalSignPageController => _vitalSignPageController; + + int _vitalSignCurrentPage = 0; + int get vitalSignCurrentPage => _vitalSignCurrentPage; + + void setVitalSignCurrentPage(int page) { + _vitalSignCurrentPage = page; + notifyListeners(); + } + + + // HHC specific lists + List hhcOrdersList = []; + List hhcServicesList = []; + + // CMC order creation state + HospitalsModel? selectedHospitalForOrder; + GetCMCServicesResponseModel? selectedServiceForOrder; + + // HHC order creation state (no hospital selection needed for home healthcare) + GetCMCServicesResponseModel? selectedServiceForHhcOrder; + + List relationTypes = []; + List getAllCitiesList = []; + List searchReferralList = []; + List covidTestProcedureList = []; + Covid19GetPaymentInfo? covidPaymentInfo; + Future getOrdersList() async {} + + + + // HHC multiple services selection + List selectedHhcServices = []; + + Future getCmcOrdersList() async { + cmcOrdersList.clear(); + isCmcOrdersLoading = true; + notifyListeners(); + await getAllCmcOrders(); + } + + // Helper to sort hospitals by distance (ascending). Safely converts distanceinkMS to double. + void _sortHospitalsByDistance(List list) { + double toDouble(dynamic v) { + if (v == null) return double.infinity; + if (v is num) return v.toDouble(); + if (v is String) return double.tryParse(v) ?? double.infinity; + return double.infinity; + } + + list.sort((a, b) { + final da = toDouble(a.distanceInKilometers); + final db = toDouble(b.distanceInKilometers); + return da.compareTo(db); + }); + } + + Future getAllCmcOrders({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isCmcOrdersLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getAllComprehensiveCheckupOrders(); + + result.fold( + (failure) async { + isCmcOrdersLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isCmcOrdersLoading = false; + if (apiResponse.messageStatus == 1) { + cmcOrdersList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future getAllCmcServices({required int patientID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + isCmcServicesLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getAllCmcServices(patientID: patientID); + + result.fold( + (failure) async { + isCmcServicesLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isCmcServicesLoading = false; + if (apiResponse.messageStatus == 1) { + cmcServicesList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future updateCmcPresOrder({ + required OrderUpdateRequestModel requestModel, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isUpdatingOrder = true; + notifyListeners(); + + final result = await hmgServicesRepo.updateCmcPresOrder(requestModel); + + bool success = false; + + result.fold( + (failure) async { + isUpdatingOrder = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdatingOrder = false; + if (apiResponse.messageStatus == 1) { + success = true; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + + return success; + } + + Future getHospitalsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isHospitalListLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getHospitalsList(); + + result.fold( + (failure) async { + isHospitalListLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isHospitalListLoading = false; + if (apiResponse.messageStatus == 1) { + hospitalsList = apiResponse.data ?? []; + filteredHospitalsList = List.from(hospitalsList); + // ensure hospitals are sorted by distance before showing + _sortHospitalsByDistance(filteredHospitalsList); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + void filterHospitalsByString(String searchText, bool isArabic) { + if (searchText.isEmpty) { + filteredHospitalsList = List.from(hospitalsList); + _sortHospitalsByDistance(filteredHospitalsList); + } else { + filteredHospitalsList = hospitalsList.where((HospitalsModel hospital) { + final name = isArabic ? (hospital.nameN ?? '') : (hospital.name ?? ''); + return name.toLowerCase().contains(searchText.toLowerCase()); + }).toList(); + _sortHospitalsByDistance(filteredHospitalsList); + } + notifyListeners(); + } + + void setSelectedHospital(HospitalsModel? hospital) { + selectedHospital = hospital; + notifyListeners(); + } + + void clearHospitalSelection() { + selectedHospital = null; + filteredHospitalsList = List.from(hospitalsList); + _sortHospitalsByDistance(filteredHospitalsList); + notifyListeners(); + } + + // CMC Order management methods + void setSelectedHospitalForOrder(HospitalsModel? hospital) { + selectedHospitalForOrder = hospital; + notifyListeners(); + } + + void setSelectedServiceForOrder(GetCMCServicesResponseModel? service) { + selectedServiceForOrder = service; + notifyListeners(); + } + + void clearOrderSelection() { + selectedHospitalForOrder = null; + selectedServiceForOrder = null; + notifyListeners(); + } + + bool get isOrderReadyToConfirm => selectedHospitalForOrder != null && selectedServiceForOrder != null; + + Future addCmcOrder({ + required int projectID, + required int orderServiceID, + required List services, + Function(int)? onSuccess, + Function(String)? onError, + }) async { + isUpdatingOrder = true; + notifyListeners(); + + final result = await hmgServicesRepo.addCmcOrder( + projectID: projectID, + orderServiceID: orderServiceID, + services: services, + ); + + int requestId = 0; + + result.fold( + (failure) async { + isUpdatingOrder = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdatingOrder = false; + if (apiResponse.messageStatus == 1) { + requestId = apiResponse.data ?? 0; + notifyListeners(); + if (onSuccess != null) { + onSuccess(requestId); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + + return requestId; + } + +// ******************* HOME HEALTHCARE APIs ******************** + + Future getHhcOrdersList() async { + hhcOrdersList.clear(); + isHhcOrdersLoading = true; + notifyListeners(); + await getAllHhcOrders(); + } + + Future getAllHhcOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isHhcOrdersLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getAllHomeHealthCareCheckupOrders(); + + result.fold( + (failure) async { + isHhcOrdersLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isHhcOrdersLoading = false; + if (apiResponse.messageStatus == 1) { + hhcOrdersList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future getAllHhcServices({required int patientID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + isHhcServicesLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getAllHhcServices(patientID: patientID); + + result.fold( + (failure) async { + isHhcServicesLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isHhcServicesLoading = false; + if (apiResponse.messageStatus == 1) { + hhcServicesList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future updateHhcPresOrder({ + required OrderUpdateRequestModel requestModel, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isUpdatingOrder = true; + notifyListeners(); + + final result = await hmgServicesRepo.updateHhcPresOrder(requestModel); + + bool success = false; + + result.fold( + (failure) async { + isUpdatingOrder = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdatingOrder = false; + if (apiResponse.messageStatus == 1) { + success = true; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + + return success; + } + + Future addHhcOrder({ + required int projectID, + required int orderServiceID, + required List services, + Function(int)? onSuccess, + Function(String)? onError, + }) async { + isUpdatingOrder = true; + notifyListeners(); + + final result = await hmgServicesRepo.addHhcOrder( + projectID: projectID, + orderServiceID: orderServiceID, + services: services, + ); + + int requestId = 0; + + result.fold( + (failure) async { + isUpdatingOrder = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdatingOrder = false; + if (apiResponse.messageStatus == 1) { + requestId = apiResponse.data ?? 0; + notifyListeners(); + if (onSuccess != null) { + onSuccess(requestId); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + + return requestId; + } + + // HHC Order management methods (no hospital selection for home healthcare) + void setSelectedServiceForHhcOrder(GetCMCServicesResponseModel? service) { + selectedServiceForHhcOrder = service; + notifyListeners(); + } + + void clearHhcOrderSelection() { + selectedServiceForHhcOrder = null; + selectedHhcServices.clear(); + notifyListeners(); + } + + bool get isHhcOrderReadyToConfirm => selectedServiceForHhcOrder != null; + + // Multiple HHC services selection methods + void toggleHhcServiceSelection(GetCMCServicesResponseModel service) { + final index = selectedHhcServices.indexWhere((s) => s.iD == service.iD); + if (index != -1) { + selectedHhcServices.removeAt(index); + } else { + selectedHhcServices.add(service); + } + notifyListeners(); + } + + bool isHhcServiceSelected(GetCMCServicesResponseModel service) { + return selectedHhcServices.any((s) => s.iD == service.iD); + } + + double getHhcSelectedServicesTotal() { + double total = 0.0; + for (var service in selectedHhcServices) { + total += (service.priceTotal ?? 0); + } + return total; + } + + void clearHhcServicesSelection() { + selectedHhcServices.clear(); + notifyListeners(); + } + + /*e-referral functions*/ + + Future getRelationshipType({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + final result = await hmgServicesRepo.getRelationshipTypes(); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + relationTypes = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future getAllCities({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + final result = await hmgServicesRepo.getAllCities(); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + getAllCitiesList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future eReferralSendActivationCode({ + required SendActivationCodeForEReferralRequestModel requestModel, + Function(GenericApiModel)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.sendEReferralActivationCode(requestModel); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future checkEReferralActivationCode({ + required CheckActivationCodeForEReferralRequestModel requestModel, + Function(GenericApiModel)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.checkEReferralActivationCode(requestModel); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future createEReferral({ + required CreateEReferralRequestModel requestModel, + Function(GenericApiModel)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.createEReferral(requestModel); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future searchEReferral({ + required SearchEReferralRequestModel requestModel, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.searchEReferral(requestModel); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + searchReferralList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future navigateToOTPScreen({ + required OTPTypeEnum otpTypeEnum, + required String phoneNumber, + required String loginToken, + required Function onSuccess, + }) async { + navigationService.pushToOtpScreen( + phoneNumber: phoneNumber, + isFormFamilyFile: false, + checkActivationCode: (int activationCode) async { + checkEReferralActivationCode( + requestModel: CheckActivationCodeForEReferralRequestModel( + logInTokenID: loginToken, + activationCode: activationCode.toString(), + ), + onSuccess: (GenericApiModel response) { + onSuccess(); + }, + onError: (String errorMessage) { + print(errorMessage); + }, + ); + }, + onResendOTPPressed: (String phoneNumber) async { + // await sendActivationCode( + // otpTypeEnum: otpTypeEnum, + // phoneNumber: phoneNumberController.text, + // nationalIdOrFileNumber: nationalIdController.text, + // isForRegister: isComingFromRegister, + // isComingFromResendOTP: true, + // payload: payload, + // isFormFamilyFile: isFormFamilyFile, + // isExcludedUser: isExcludedUser, + // responseID: responseID, + // ); + }, + ); + } + List getQuestionsFromJson() { + final String questionsJson = '''[ { "id": 1, "questionEN": "Is the test intended for travel?", "questionAR": "هل تجري التحليل بغرض السفر؟", "ans": 2 }, { "id": 2, "questionEN": "Coming from outside KSA within last 2 weeks?", "questionAR": "هل قدمت من خارج المملكة خلال الأسبوعين الماضيين؟", "ans": 2 }, { "id": 3, "questionEN": "Do you currently have fever?", "questionAR": "هل تعاني حاليا من حرارة؟", "ans": 2 }, { "id": 4, "questionEN": "Did you have fever in last 2 weeks?", "questionAR": "هل عانيت من حرارة في الأسبوعين الماضيين؟", "ans": 2 }, { "id": 5, "questionEN": "Do you have a sore throat?", "questionAR": "هل لديك التهاب في الحلق؟", "ans": 2 }, { "id": 6, "questionEN": "Do you have a runny nose?", "questionAR": "هل لديك سيلان بالأنف؟" }, { "id": 7, "questionEN": "Do you have a cough?", "questionAR": "هل لديك سعال؟", "ans": 2 }, { "id": 8, "questionEN": "Do you have shortness of breath?", "questionAR": "هل تعاني من ضيق في التنفس؟", "ans": 2 }, { "id": 9, "questionEN": "Do you have nausea?", "questionAR": "هل تعاني من غثيان؟", "ans": 2 }, { "id": 10, "questionEN": "Do you have vomiting?", "questionAR": "هل تعاني من القيء؟", "ans": 2 }, { "id": 11, "questionEN": "Do you have a headache?", "questionAR": "هل تعاني من صداع في الرأس؟", "ans": 2 }, { "id": 12, "questionEN": "Do you have muscle pain?", "questionAR": "هل تعانين من آلام عضلية؟", "ans": 2 }, { "id": 13, "questionEN": "Do you have joint pain?", "questionAR": "هل تعاني من آلام المفاصل؟", "ans": 2 }, { "id": 14, "questionEN": "Do you have diarrhea?", "questionAR": "هل لديك اسهال؟", "ans": 2 } ]'''; + + try { + final parsed = json.decode(questionsJson) as List; + return parsed + .map((e) => CovidQuestionnaireModel.fromJson(Map.from(e))) + .toList(); + } catch (_) { + return []; + } + } + + + Future getCovidProcedureList({ + + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.getCovidTestProcedures(); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + covidTestProcedureList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + + Future getPaymentInfo({ + String? procedureID, + int? projectID, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.getCovidPaymentInfo(procedureID!, projectID!); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + covidPaymentInfo = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future getPatientVitalSign({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isVitalSignLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getPatientVitalSign(); + + result.fold( + (failure) async { + isVitalSignLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isVitalSignLoading = false; + if (apiResponse.messageStatus == 1) { + vitalSignList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + @override + void dispose() { + _vitalSignPageController.dispose(); + super.dispose(); + } +} diff --git a/lib/features/hmg_services/models/req_models/check_activation_e_referral_req_model.dart b/lib/features/hmg_services/models/req_models/check_activation_e_referral_req_model.dart new file mode 100644 index 0000000..a356616 --- /dev/null +++ b/lib/features/hmg_services/models/req_models/check_activation_e_referral_req_model.dart @@ -0,0 +1,57 @@ +class CheckActivationCodeForEReferralRequestModel { + String? logInTokenID; + String? activationCode; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + dynamic sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + + CheckActivationCodeForEReferralRequestModel( + {this.logInTokenID, + this.activationCode, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID}); + + CheckActivationCodeForEReferralRequestModel.fromJson( + Map json) { + logInTokenID = json['LogInTokenID']; + activationCode = json['activationCode']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + } + + Map toJson() { + final Map data = new Map(); + data['LogInTokenID'] = this.logInTokenID; + data['activationCode'] = this.activationCode; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart b/lib/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart new file mode 100644 index 0000000..a1fcc4b --- /dev/null +++ b/lib/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart @@ -0,0 +1,131 @@ +class CMCInsertPresOrderRequestModel { + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + double? latitude; + double? longitude; + int? createdBy; + int? orderServiceID; + int? projectID; + List? patientERCMCInsertServicesList; + + CMCInsertPresOrderRequestModel( + {this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType, + this.latitude, + this.longitude, + this.createdBy, + this.orderServiceID, + this.projectID, + this.patientERCMCInsertServicesList}); + + CMCInsertPresOrderRequestModel.fromJson(Map json) { + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + latitude = json['Latitude']; + longitude = json['Longitude']; + createdBy = json['CreatedBy']; + orderServiceID = json['OrderServiceID']; + projectID = json['ProjectId']; + if (json['PatientER_CMC_InsertServicesList'] != null) { + patientERCMCInsertServicesList = []; + json['PatientER_CMC_InsertServicesList'].forEach((v) { + patientERCMCInsertServicesList!.add( + new PatientERCMCInsertServicesList.fromJson(v), + ); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['isOutPatient'] = this.patientOutSA == 0 ? false : true; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + data['latitude'] = this.latitude; + data['longitude'] = this.longitude; + // data['CreatedBy'] = this.createdBy; + data['OrderServiceID'] = this.orderServiceID; + data['ProjectID'] = this.projectID; + if (this.patientERCMCInsertServicesList != null) { + data['procedures'] = this.patientERCMCInsertServicesList!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PatientERCMCInsertServicesList { + int? recordID; + String? serviceID; + String? selectedServiceName; + String? selectedServiceNameAR; + dynamic price; + dynamic vAT; + dynamic totalPrice; + + PatientERCMCInsertServicesList( + {this.recordID, this.serviceID, this.selectedServiceName, this.selectedServiceNameAR, this.price, this.vAT, this.totalPrice}); + + PatientERCMCInsertServicesList.fromJson(Map json) { + recordID = json['RecordID']; + serviceID = json['ServiceID']; + selectedServiceName = json['selectedServiceName']; + selectedServiceNameAR = json['selectedServiceNameAR']; + price = json['Price']; + vAT = json['VAT']; + totalPrice = json['TotalPrice']; + } + + Map toJson() { + final Map data = new Map(); + data['RecordID'] = this.recordID; + data['ServiceID'] = this.serviceID; + data['selectedServiceName'] = this.selectedServiceName; + data['selectedServiceNameAR'] = this.selectedServiceNameAR; + data['Price'] = this.price; + data['VAT'] = this.vAT; + data['TotalPrice'] = this.totalPrice; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/cmc_create_service_order_req_model.dart b/lib/features/hmg_services/models/req_models/cmc_create_service_order_req_model.dart new file mode 100644 index 0000000..af2808d --- /dev/null +++ b/lib/features/hmg_services/models/req_models/cmc_create_service_order_req_model.dart @@ -0,0 +1,41 @@ +class CmcCreateServiceOrderReqModel { + int? recordID; + String? serviceID; + String? selectedServiceName; + String? selectedServiceNameAR; + dynamic price; + dynamic vAT; + dynamic totalPrice; + + CmcCreateServiceOrderReqModel({ + this.recordID, + this.serviceID, + this.selectedServiceName, + this.selectedServiceNameAR, + this.price, + this.vAT, + this.totalPrice, + }); + + CmcCreateServiceOrderReqModel.fromJson(Map json) { + recordID = json['RecordID']; + serviceID = json['ServiceID']; + selectedServiceName = json['selectedServiceName']; + selectedServiceNameAR = json['selectedServiceNameAR']; + price = json['Price']; + vAT = json['VAT']; + totalPrice = json['TotalPrice']; + } + + Map toJson() { + final Map data = {}; + data['RecordID'] = recordID; + data['ServiceID'] = serviceID; + data['selectedServiceName'] = selectedServiceName; + data['selectedServiceNameAR'] = selectedServiceNameAR; + data['Price'] = price; + data['VAT'] = vAT; + data['TotalPrice'] = totalPrice; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/create_e_referral_model.dart b/lib/features/hmg_services/models/req_models/create_e_referral_model.dart new file mode 100644 index 0000000..1b351fd --- /dev/null +++ b/lib/features/hmg_services/models/req_models/create_e_referral_model.dart @@ -0,0 +1,155 @@ +class CreateEReferralRequestModel { + bool? isInsuredPatient; + String? cityCode; + String? cityName; + String? requesterName; + String? requesterContactNo; + int? requesterRelationship; + String? otherRelationship; + String? fullName; + int? identificationNo; + String? patientMobileNumber; + int? preferredBranchCode; + String? preferredBranchName; + List? medicalReportAttachment; + dynamic insuranceCardAttachment; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + + CreateEReferralRequestModel( + {this.isInsuredPatient, + this.cityCode, + this.cityName, + this.requesterName, + this.requesterContactNo, + this.requesterRelationship, + this.otherRelationship, + this.fullName, + this.identificationNo, + this.patientMobileNumber, + this.preferredBranchCode, + this.preferredBranchName, + this.medicalReportAttachment, + this.insuranceCardAttachment, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType}); + + CreateEReferralRequestModel.fromJson(Map json) { + isInsuredPatient = json['IsInsuredPatient']; + cityCode = json['CityCode']; + cityName = json['CityName']; + requesterName = json['RequesterName']; + requesterContactNo = json['RequesterContactNo']; + requesterRelationship = json['RequesterRelationship']; + otherRelationship = json['OtherRelationship']; + fullName = json['FullName']; + identificationNo = json['IdentificationNo']; + patientMobileNumber = json['PatientMobileNumber']; + preferredBranchCode = json['PreferredBranchCode']; + preferredBranchName = json['PreferredBranchName']; + if (json['MedicalReportAttachment'] != null) { + medicalReportAttachment = []; + json['MedicalReportAttachment'].forEach((v) { + medicalReportAttachment!.add(EReferralAttachment.fromJson(v)); + }); + } + insuranceCardAttachment = json['InsuranceCardAttachment'] != null ? EReferralAttachment.fromJson(json['InsuranceCardAttachment']) : null; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + } + + Map toJson() { + final Map data = new Map(); + data['IsInsuredPatient'] = this.isInsuredPatient; + data['CityCode'] = this.cityCode; + data['CityName'] = this.cityName; + data['RequesterName'] = this.requesterName; + data['RequesterContactNo'] = this.requesterContactNo; + data['RequesterRelationship'] = this.requesterRelationship; + data['OtherRelationship'] = this.otherRelationship; + data['FullName'] = this.fullName; + data['IdentificationNo'] = this.identificationNo; + data['PatientMobileNumber'] = this.patientMobileNumber; + data['PreferredBranchCode'] = this.preferredBranchCode; + data['PreferredBranchName'] = this.preferredBranchName; + if (this.medicalReportAttachment != null) { + // FIXED: Use map() to convert each item to JSON, then convert to list + data['MedicalReportAttachment'] = this.medicalReportAttachment!.map((v) => v.toJson()).toList(); + } + if (this.insuranceCardAttachment == null) { + data['InsuranceCardAttachment'] = {}; + } else if (this.insuranceCardAttachment is EReferralAttachment) { + // FIXED: Check if it's an EReferralAttachment before calling toJson() + data['InsuranceCardAttachment'] = (this.insuranceCardAttachment as EReferralAttachment).toJson(); + } else { + // If it's something else, assign directly + data['InsuranceCardAttachment'] = this.insuranceCardAttachment; + } + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientID'] = this.patientID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + return data; + } +} + +class EReferralAttachment { + String? fileName; + String? base64String; + + EReferralAttachment({this.fileName, this.base64String}); + + EReferralAttachment.fromJson(Map json) { + fileName = json['FileName']; + base64String = json['Base64String']; + } + + Map toJson() { + final Map data = new Map(); + data['FileName'] = this.fileName; + data['Base64String'] = this.base64String; + return data; + } +} \ No newline at end of file diff --git a/lib/features/hmg_services/models/req_models/create_e_referral_req_model.dart b/lib/features/hmg_services/models/req_models/create_e_referral_req_model.dart new file mode 100644 index 0000000..0672fe6 --- /dev/null +++ b/lib/features/hmg_services/models/req_models/create_e_referral_req_model.dart @@ -0,0 +1,150 @@ +class CreateEReferralRequestModel { + bool? isInsuredPatient; + String? cityCode; + String? cityName; + String? requesterName; + String? requesterContactNo; + int? requesterRelationship; + String? otherRelationship; + String? fullName; + int? identificationNo; + String? patientMobileNumber; + int? preferredBranchCode; + String? preferredBranchName; + List? medicalReportAttachment; + dynamic insuranceCardAttachment; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + + CreateEReferralRequestModel( + {this.isInsuredPatient, + this.cityCode, + this.cityName, + this.requesterName, + this.requesterContactNo, + this.requesterRelationship, + this.otherRelationship, + this.fullName, + this.identificationNo, + this.patientMobileNumber, + this.preferredBranchCode, + this.preferredBranchName, + this.medicalReportAttachment, + this.insuranceCardAttachment, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType}); + + CreateEReferralRequestModel.fromJson(Map json) { + isInsuredPatient = json['IsInsuredPatient']; + cityCode = json['CityCode']; + cityName = json['CityName']; + requesterName = json['RequesterName']; + requesterContactNo = json['RequesterContactNo']; + requesterRelationship = json['RequesterRelationship']; + otherRelationship = json['OtherRelationship']; + fullName = json['FullName']; + identificationNo = json['IdentificationNo']; + patientMobileNumber = json['PatientMobileNumber']; + preferredBranchCode = json['PreferredBranchCode']; + preferredBranchName = json['PreferredBranchName']; + if (json['MedicalReportAttachment'] != null) { + medicalReportAttachment = []; + json['MedicalReportAttachment'].forEach((v) { + medicalReportAttachment!.add(EReferralAttachment.fromJson(v)); + }); + } + insuranceCardAttachment = json['InsuranceCardAttachment'] != null ? EReferralAttachment.fromJson(json['InsuranceCardAttachment']) : null; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + } + + Map toJson() { + final Map data = new Map(); + data['IsInsuredPatient'] = this.isInsuredPatient; + data['CityCode'] = this.cityCode; + data['CityName'] = this.cityName; + data['RequesterName'] = this.requesterName; + data['RequesterContactNo'] = this.requesterContactNo; + data['RequesterRelationship'] = this.requesterRelationship; + data['OtherRelationship'] = this.otherRelationship; + data['FullName'] = this.fullName; + data['IdentificationNo'] = this.identificationNo; + data['PatientMobileNumber'] = this.patientMobileNumber; + data['PreferredBranchCode'] = this.preferredBranchCode; + data['PreferredBranchName'] = this.preferredBranchName; + if (this.medicalReportAttachment != null) { + data['MedicalReportAttachment'] = this.medicalReportAttachment!.map((v) => v.toJson()).toList(); + } + if (this.insuranceCardAttachment == null) { + data['InsuranceCardAttachment'] = {}; + } else + data['InsuranceCardAttachment'] = this.insuranceCardAttachment.toJson(); + + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientID'] = this.patientID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + return data; + } +} + +class EReferralAttachment { + String? fileName; + String? base64String; + + EReferralAttachment({this.fileName, this.base64String}); + + EReferralAttachment.fromJson(Map json) { + fileName = json['FileName']; + base64String = json['Base64String']; + } + + Map toJson() { + final Map data = new Map(); + data['FileName'] = this.fileName; + data['Base64String'] = this.base64String; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/order_update_req_model.dart b/lib/features/hmg_services/models/req_models/order_update_req_model.dart new file mode 100644 index 0000000..0f9964f --- /dev/null +++ b/lib/features/hmg_services/models/req_models/order_update_req_model.dart @@ -0,0 +1,82 @@ +class OrderUpdateRequestModel { + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + int? presOrderID; + int? presOrderStatus; + int? editedBy; + String? rejectionReason; + + OrderUpdateRequestModel({ + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType, + this.presOrderID, + this.presOrderStatus, + this.editedBy, + this.rejectionReason, + }); + + OrderUpdateRequestModel.fromJson(Map json) { + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + presOrderID = json['PresOrderID']; + presOrderStatus = json['PresOrderStatus']; + editedBy = json['EditedBy']; + rejectionReason = json['RejectionReason']; + } + + Map toJson() { + final Map data = {}; + data['VersionID'] = versionID; + data['Channel'] = channel; + data['LanguageID'] = languageID; + data['IPAdress'] = iPAdress; + data['generalid'] = generalid; + data['PatientOutSA'] = patientOutSA; + data['SessionID'] = sessionID; + data['isDentalAllowedBackend'] = isDentalAllowedBackend; + data['DeviceTypeID'] = deviceTypeID; + data['PatientID'] = patientID; + data['TokenID'] = tokenID; + data['PatientTypeID'] = patientTypeID; + data['PatientType'] = patientType; + data['Id'] = presOrderID; + data['ClickButton'] = 14; + data['PresOrderStatus'] = presOrderStatus; + data['EditedBy'] = editedBy; + data['RejectionReason'] = rejectionReason; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/search_e_referral_req_model.dart b/lib/features/hmg_services/models/req_models/search_e_referral_req_model.dart new file mode 100644 index 0000000..deb3a92 --- /dev/null +++ b/lib/features/hmg_services/models/req_models/search_e_referral_req_model.dart @@ -0,0 +1,60 @@ +class SearchEReferralRequestModel { + String? patientMobileNumber; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + dynamic sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? referralNumber; + String? identificationNo; + + SearchEReferralRequestModel( + {this.patientMobileNumber, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.referralNumber, + this.identificationNo}); + + SearchEReferralRequestModel.fromJson(Map json) { + patientMobileNumber = json['PatientMobileNumber']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + referralNumber = json['ReferralNumber']; + identificationNo = json['IdentificationNo']; + } + + Map toJson() { + final Map data = new Map(); + data['PatientMobileNumber'] = this.patientMobileNumber; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['ReferralNumber'] = this.referralNumber; + data['IdentificationNo'] = this.identificationNo; + return data; + } +} diff --git a/lib/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart b/lib/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart new file mode 100644 index 0000000..0380de8 --- /dev/null +++ b/lib/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart @@ -0,0 +1,56 @@ +class SendActivationCodeForEReferralRequestModel { + int? patientMobileNumber; + String? zipCode; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + dynamic sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + + SendActivationCodeForEReferralRequestModel( + {this.patientMobileNumber, + this.zipCode, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID}); + + SendActivationCodeForEReferralRequestModel.fromJson(Map json) { + patientMobileNumber = json['PatientMobileNumber']; + zipCode = json['ZipCode']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + } + + Map toJson() { + final Map data = new Map(); + data['PatientMobileNumber'] = this.patientMobileNumber; + data['ZipCode'] = this.zipCode; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + return data; + } +} diff --git a/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart b/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart new file mode 100644 index 0000000..c27f1e7 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +class Covid19GetTestProceduresResp { + String? procedureId; + String? procedureName; + String? procedureNameN; + String? setupId; + + Covid19GetTestProceduresResp({ + this.procedureId, + this.procedureName, + this.procedureNameN, + this.setupId, + }); + + factory Covid19GetTestProceduresResp.fromRawJson(String str) => Covid19GetTestProceduresResp.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Covid19GetTestProceduresResp.fromJson(Map json) => Covid19GetTestProceduresResp( + procedureId: json["ProcedureID"], + procedureName: json["ProcedureName"], + procedureNameN: json["ProcedureNameN"], + setupId: json["SetupID"], + ); + + Map toJson() => { + "ProcedureID": procedureId, + "ProcedureName": procedureName, + "ProcedureNameN": procedureNameN, + "SetupID": setupId, + }; +} diff --git a/lib/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart b/lib/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart new file mode 100644 index 0000000..efc3d58 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart @@ -0,0 +1,21 @@ +class GetAllCitiesResponseModel { + int? iD; + String? description; + String? descriptionN; + + GetAllCitiesResponseModel({this.iD, this.description, this.descriptionN}); + + GetAllCitiesResponseModel.fromJson(Map json) { + iD = json['ID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['Description'] = this.description; + data['DescriptionN'] = this.descriptionN; + return data; + } +} diff --git a/lib/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart b/lib/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart new file mode 100644 index 0000000..ddd91f4 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart @@ -0,0 +1,344 @@ +import 'dart:developer'; + +class GetCMCAllOrdersResponseModel { + int? iD; + int? patientId; + int? patientOutSa; + bool? isOutPatient; + int? projectId; + int? nearestProjectId; + dynamic longitude; + dynamic latitude; + dynamic appointmentNo; + dynamic dischargeId; + int? statusId; + int? serviceId; + int? channel; + Orderpayment? orderpayment; + dynamic orderselectedservice; + dynamic wforder; + dynamic orderapprovalobj; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + String? statusText; + int? paymentStatus; + dynamic clientRequestid; + dynamic paymentStatusText; + String? projectName; + String? nearestProjectName; + dynamic paymentAmount; + WFOrder? wFOrder; + String? serviceText; + bool? isSentForApproval; + int? exaCartOrderId; + bool? isTimer; + int? timeSeconds; + int? totalPendingSeconds; + int? timeMinute; + int? timeHour; + int? timeTotalSeconds; + int? timeTotalMinute; + int? timeTotalHour; + dynamic approvalStatus; + bool? isActive; + int? clickButton; + List? procedures; + dynamic pickupLocation; + dynamic dropOffLocation; + dynamic clinicName; + dynamic doctorName; + dynamic branch; + dynamic time; + dynamic notes; + + GetCMCAllOrdersResponseModel( + {this.iD, + this.patientId, + this.patientOutSa, + this.isOutPatient, + this.projectId, + this.nearestProjectId, + this.longitude, + this.latitude, + this.appointmentNo, + this.dischargeId, + this.statusId, + this.serviceId, + this.channel, + this.orderpayment, + this.orderselectedservice, + this.wforder, + this.orderapprovalobj, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted, + this.statusText, + this.paymentStatus, + this.clientRequestid, + this.paymentStatusText, + this.projectName, + this.nearestProjectName, + this.paymentAmount, + this.wFOrder, + this.serviceText, + this.isSentForApproval, + this.exaCartOrderId, + this.isTimer, + this.timeSeconds, + this.totalPendingSeconds, + this.timeMinute, + this.timeHour, + this.timeTotalSeconds, + this.timeTotalMinute, + this.timeTotalHour, + this.approvalStatus, + this.isActive, + this.clickButton, + this.procedures, + this.pickupLocation, + this.dropOffLocation, + this.clinicName, + this.doctorName, + this.branch, + this.time, + this.notes}); + + GetCMCAllOrdersResponseModel.fromJson(Map json) { + log("responseJson: $json"); + iD = json['ID']; + patientId = json['PatientId']; + patientOutSa = json['PatientOutSa']; + isOutPatient = json['IsOutPatient']; + projectId = json['ProjectId']; + nearestProjectId = json['NearestProjectId']; + longitude = json['Longitude']; + latitude = json['Latitude']; + appointmentNo = json['AppointmentNo']; + dischargeId = json['DischargeId']; + statusId = json['StatusId']; + serviceId = json['ServiceId']; + channel = json['Channel']; + orderpayment = json['orderpayment'] != null ? Orderpayment.fromJson(json['orderpayment']) : null; + orderselectedservice = json['orderselectedservice']; + wforder = json['wforder']; + orderapprovalobj = json['orderapprovalobj']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + statusText = json['StatusText']; + paymentStatus = json['PaymentStatus']; + clientRequestid = json['ClientRequestid']; + paymentStatusText = json['PaymentStatusText']; + projectName = json['ProjectName']; + nearestProjectName = json['NearestProjectName']; + paymentAmount = json['PaymentAmount']; + wFOrder = json['WF_order'] != null ? WFOrder.fromJson(json['WF_order']) : null; + serviceText = json['ServiceText']; + isSentForApproval = json['isSentForApproval']; + exaCartOrderId = json['ExaCart_OrderId']; + isTimer = json['isTimer']; + timeSeconds = json['TimeSeconds']; + totalPendingSeconds = json['TotalPendingSeconds']; + timeMinute = json['TimeMinute']; + timeHour = json['TimeHour']; + timeTotalSeconds = json['TimeTotalSeconds']; + timeTotalMinute = json['TimeTotalMinute']; + timeTotalHour = json['TimeTotalHour']; + approvalStatus = json['ApprovalStatus']; + isActive = json['isActive']; + clickButton = json['ClickButton']; + pickupLocation = json['PickupLocation']; + dropOffLocation = json['DropOffLocation']; + clinicName = json['clinicName']; + doctorName = json['DoctorName']; + branch = json['Branch']; + time = json['Time']; + notes = json['Notes']; + } + + Map toJson() { + final Map data = {}; + data['ID'] = iD; + data['PatientId'] = patientId; + data['PatientOutSa'] = patientOutSa; + data['IsOutPatient'] = isOutPatient; + data['ProjectId'] = projectId; + data['NearestProjectId'] = nearestProjectId; + data['Longitude'] = longitude; + data['Latitude'] = latitude; + data['AppointmentNo'] = appointmentNo; + data['DischargeId'] = dischargeId; + data['StatusId'] = statusId; + data['ServiceId'] = serviceId; + data['Channel'] = channel; + if (orderpayment != null) { + data['orderpayment'] = orderpayment!.toJson(); + } + data['orderselectedservice'] = orderselectedservice; + + data['wforder'] = wforder; + data['orderapprovalobj'] = orderapprovalobj; + data['Created'] = created; + data['CreatedBy'] = createdBy; + data['Modified'] = modified; + data['ModifiedBy'] = modifiedBy; + data['IsDeleted'] = isDeleted; + data['StatusText'] = statusText; + data['PaymentStatus'] = paymentStatus; + data['ClientRequestid'] = clientRequestid; + data['PaymentStatusText'] = paymentStatusText; + data['ProjectName'] = projectName; + data['NearestProjectName'] = nearestProjectName; + data['PaymentAmount'] = paymentAmount; + if (wFOrder != null) { + data['WF_order'] = wFOrder!.toJson(); + } + data['ServiceText'] = serviceText; + data['isSentForApproval'] = isSentForApproval; + data['ExaCart_OrderId'] = exaCartOrderId; + data['isTimer'] = isTimer; + data['TimeSeconds'] = timeSeconds; + data['TotalPendingSeconds'] = totalPendingSeconds; + data['TimeMinute'] = timeMinute; + data['TimeHour'] = timeHour; + data['TimeTotalSeconds'] = timeTotalSeconds; + data['TimeTotalMinute'] = timeTotalMinute; + data['TimeTotalHour'] = timeTotalHour; + data['ApprovalStatus'] = approvalStatus; + data['isActive'] = isActive; + data['ClickButton'] = clickButton; + data['PickupLocation'] = pickupLocation; + data['DropOffLocation'] = dropOffLocation; + data['clinicName'] = clinicName; + data['DoctorName'] = doctorName; + data['Branch'] = branch; + data['Time'] = time; + data['Notes'] = notes; + return data; + } +} + +class Orderpayment { + int? iD; + int? orderId; + dynamic clientRequestId; + dynamic totalAmount; + int? paymentStatus; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + Orderpayment( + {this.iD, + this.orderId, + this.clientRequestId, + this.totalAmount, + this.paymentStatus, + this.order, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted}); + + Orderpayment.fromJson(Map json) { + iD = json['ID']; + orderId = json['OrderId']; + clientRequestId = json['ClientRequestId']; + totalAmount = json['TotalAmount']; + paymentStatus = json['PaymentStatus']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = {}; + data['ID'] = iD; + data['OrderId'] = orderId; + data['ClientRequestId'] = clientRequestId; + data['TotalAmount'] = totalAmount; + data['PaymentStatus'] = paymentStatus; + data['Order'] = order; + data['Created'] = created; + data['CreatedBy'] = createdBy; + data['Modified'] = modified; + data['ModifiedBy'] = modifiedBy; + data['IsDeleted'] = isDeleted; + return data; + } +} + +class WFOrder { + dynamic wfButtonsDTO; + int? iD; + int? orderId; + int? previousStep; + int? nextStep; + int? serviceId; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + WFOrder( + {this.wfButtonsDTO, + this.iD, + this.orderId, + this.previousStep, + this.nextStep, + this.serviceId, + this.order, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted}); + + WFOrder.fromJson(Map json) { + wfButtonsDTO = json['wf_ButtonsDTO']; + iD = json['ID']; + orderId = json['OrderId']; + previousStep = json['PreviousStep']; + nextStep = json['NextStep']; + serviceId = json['ServiceId']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = {}; + data['wf_ButtonsDTO'] = wfButtonsDTO; + data['ID'] = iD; + data['OrderId'] = orderId; + data['PreviousStep'] = previousStep; + data['NextStep'] = nextStep; + data['ServiceId'] = serviceId; + data['Order'] = order; + data['Created'] = created; + data['CreatedBy'] = createdBy; + data['Modified'] = modified; + data['ModifiedBy'] = modifiedBy; + data['IsDeleted'] = isDeleted; + return data; + } +} diff --git a/lib/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart b/lib/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart new file mode 100644 index 0000000..670a582 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart @@ -0,0 +1,57 @@ +class GetCMCServicesResponseModel { + int? iD; + String? serviceID; + int? orderServiceID; + String? text; + String? textN; + dynamic price; + dynamic priceVAT; + dynamic priceTotal; + bool? isEnabled; + int? orderId; + int? quantity; + + GetCMCServicesResponseModel({ + this.iD, + this.serviceID, + this.orderServiceID, + this.text, + this.textN, + this.price, + this.priceVAT, + this.priceTotal, + this.isEnabled, + this.orderId, + this.quantity, + }); + + GetCMCServicesResponseModel.fromJson(Map json) { + iD = json['ID']; + serviceID = json['ServiceID']; + orderServiceID = json['OrderServiceID']; + text = json['Text']; + textN = json['TextN']; + price = json['Price']; + priceVAT = json['PriceVAT']; + priceTotal = json['PriceTotal']; + isEnabled = json['IsEnabled']; + orderId = json['OrderId']; + quantity = json['Quantity']; + } + + Map toJson() { + final Map data = {}; + data['ID'] = this.iD; + data['ServiceID'] = this.serviceID; + data['OrderServiceID'] = this.orderServiceID; + data['Text'] = this.text; + data['TextN'] = this.textN; + data['Price'] = this.price; + data['PriceVAT'] = this.priceVAT; + data['PriceTotal'] = this.priceTotal; + data['IsEnabled'] = this.isEnabled; + data['OrderId'] = this.orderId; + data['Quantity'] = this.quantity; + return data; + } +} diff --git a/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart b/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart new file mode 100644 index 0000000..5f95263 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +class Covid19GetPaymentInfo { + dynamic propertyChanged; + int? cashPriceField; + int? cashPriceTaxField; + int? cashPriceWithTaxField; + int? companyIdField; + String? companyNameField; + int? companyShareWithTaxField; + dynamic errCodeField; + int? groupIdField; + dynamic insurancePolicyNoField; + String? messageField; + dynamic patientCardIdField; + int? patientShareField; + double? patientShareWithTaxField; + double? patientTaxAmountField; + int? policyIdField; + dynamic policyNameField; + dynamic procedureIdField; + String? procedureNameField; + dynamic setupIdField; + int? statusCodeField; + dynamic subPolicyNoField; + + Covid19GetPaymentInfo({ + this.propertyChanged, + this.cashPriceField, + this.cashPriceTaxField, + this.cashPriceWithTaxField, + this.companyIdField, + this.companyNameField, + this.companyShareWithTaxField, + this.errCodeField, + this.groupIdField, + this.insurancePolicyNoField, + this.messageField, + this.patientCardIdField, + this.patientShareField, + this.patientShareWithTaxField, + this.patientTaxAmountField, + this.policyIdField, + this.policyNameField, + this.procedureIdField, + this.procedureNameField, + this.setupIdField, + this.statusCodeField, + this.subPolicyNoField, + }); + + factory Covid19GetPaymentInfo.fromRawJson(String str) => Covid19GetPaymentInfo.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Covid19GetPaymentInfo.fromJson(Map json) => Covid19GetPaymentInfo( + propertyChanged: json["PropertyChanged"], + cashPriceField: json["cashPriceField"], + cashPriceTaxField: json["cashPriceTaxField"], + cashPriceWithTaxField: json["cashPriceWithTaxField"], + companyIdField: json["companyIdField"], + companyNameField: json["companyNameField"], + companyShareWithTaxField: json["companyShareWithTaxField"], + errCodeField: json["errCodeField"], + groupIdField: json["groupIDField"], + insurancePolicyNoField: json["insurancePolicyNoField"], + messageField: json["messageField"], + patientCardIdField: json["patientCardIDField"], + patientShareField: json["patientShareField"], + patientShareWithTaxField: json["patientShareWithTaxField"]?.toDouble(), + patientTaxAmountField: json["patientTaxAmountField"]?.toDouble(), + policyIdField: json["policyIdField"], + policyNameField: json["policyNameField"], + procedureIdField: json["procedureIdField"], + procedureNameField: json["procedureNameField"], + setupIdField: json["setupIDField"], + statusCodeField: json["statusCodeField"], + subPolicyNoField: json["subPolicyNoField"], + ); + + Map toJson() => { + "PropertyChanged": propertyChanged, + "cashPriceField": cashPriceField, + "cashPriceTaxField": cashPriceTaxField, + "cashPriceWithTaxField": cashPriceWithTaxField, + "companyIdField": companyIdField, + "companyNameField": companyNameField, + "companyShareWithTaxField": companyShareWithTaxField, + "errCodeField": errCodeField, + "groupIDField": groupIdField, + "insurancePolicyNoField": insurancePolicyNoField, + "messageField": messageField, + "patientCardIDField": patientCardIdField, + "patientShareField": patientShareField, + "patientShareWithTaxField": patientShareWithTaxField, + "patientTaxAmountField": patientTaxAmountField, + "policyIdField": policyIdField, + "policyNameField": policyNameField, + "procedureIdField": procedureIdField, + "procedureNameField": procedureNameField, + "setupIDField": setupIdField, + "statusCodeField": statusCodeField, + "subPolicyNoField": subPolicyNoField, + }; +} diff --git a/lib/features/hmg_services/models/resq_models/relationship_type_resp_mode.dart b/lib/features/hmg_services/models/resq_models/relationship_type_resp_mode.dart new file mode 100644 index 0000000..237017d --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/relationship_type_resp_mode.dart @@ -0,0 +1,25 @@ +class GetAllRelationshipTypeResponseModel { + int? iD; + String? text; + String? textAr; + String? textEn; + + GetAllRelationshipTypeResponseModel( + {this.iD, this.text, this.textAr, this.textEn}); + + GetAllRelationshipTypeResponseModel.fromJson(Map json) { + iD = json['ID']; + text = json['Text']; + textAr = json['Text_Ar']; + textEn = json['Text_En']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['Text'] = this.text; + data['Text_Ar'] = this.textAr; + data['Text_En'] = this.textEn; + return data; + } +} diff --git a/lib/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart b/lib/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart new file mode 100644 index 0000000..df72930 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; + +class SearchEReferralResponseModel { + dynamic acceptedBrachCode; + dynamic acceptedBranchName; + dynamic acceptedBranchNameAr; + dynamic channel; + dynamic identityCardAttachment; + String? identityNumber; + dynamic insuranceCardAttachment; + bool? isInsuredPatient; + List? medicalReportAttachment; + String? otherRelationship; + String? patientContactNo; + int? patientId; + String? patientName; + dynamic preferredBranchCode; + dynamic preferredBranchName; + String? referralDate; + int? referralNumber; + RelationshipType? relationshipType; + String? requesterContactNo; + String? requesterName; + String? status; + String? statusAr; + + SearchEReferralResponseModel({ + this.acceptedBrachCode, + this.acceptedBranchName, + this.acceptedBranchNameAr, + this.channel, + this.identityCardAttachment, + this.identityNumber, + this.insuranceCardAttachment, + this.isInsuredPatient, + this.medicalReportAttachment, + this.otherRelationship, + this.patientContactNo, + this.patientId, + this.patientName, + this.preferredBranchCode, + this.preferredBranchName, + this.referralDate, + this.referralNumber, + this.relationshipType, + this.requesterContactNo, + this.requesterName, + this.status, + this.statusAr, + }); + + factory SearchEReferralResponseModel.fromRawJson(String str) => SearchEReferralResponseModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory SearchEReferralResponseModel.fromJson(Map json) => SearchEReferralResponseModel( + acceptedBrachCode: json["AcceptedBrachCode"], + acceptedBranchName: json["AcceptedBranchName"], + acceptedBranchNameAr: json["AcceptedBranchNameAr"], + channel: json["Channel"], + identityCardAttachment: json["IdentityCardAttachment"], + identityNumber: json["IdentityNumber"], + insuranceCardAttachment: json["InsuranceCardAttachment"], + isInsuredPatient: json["IsInsuredPatient"], + medicalReportAttachment: json["MedicalReportAttachment"] == null ? [] : List.from(json["MedicalReportAttachment"]!.map((x) => MedicalReportAttachment.fromJson(x))), + otherRelationship: json["OtherRelationship"], + patientContactNo: json["PatientContactNo"], + patientId: json["PatientId"], + patientName: json["PatientName"], + preferredBranchCode: json["PreferredBranchCode"], + preferredBranchName: json["PreferredBranchName"], + referralDate: json["ReferralDate"], + referralNumber: json["ReferralNumber"], + relationshipType: json["RelationshipType"] == null ? null : RelationshipType.fromJson(json["RelationshipType"]), + requesterContactNo: json["RequesterContactNo"], + requesterName: json["RequesterName"], + status: json["Status"], + statusAr: json["StatusAr"], + ); + + Map toJson() => { + "AcceptedBrachCode": acceptedBrachCode, + "AcceptedBranchName": acceptedBranchName, + "AcceptedBranchNameAr": acceptedBranchNameAr, + "Channel": channel, + "IdentityCardAttachment": identityCardAttachment, + "IdentityNumber": identityNumber, + "InsuranceCardAttachment": insuranceCardAttachment, + "IsInsuredPatient": isInsuredPatient, + "MedicalReportAttachment": medicalReportAttachment == null ? [] : List.from(medicalReportAttachment!.map((x) => x.toJson())), + "OtherRelationship": otherRelationship, + "PatientContactNo": patientContactNo, + "PatientId": patientId, + "PatientName": patientName, + "PreferredBranchCode": preferredBranchCode, + "PreferredBranchName": preferredBranchName, + "ReferralDate": referralDate, + "ReferralNumber": referralNumber, + "RelationshipType": relationshipType?.toJson(), + "RequesterContactNo": requesterContactNo, + "RequesterName": requesterName, + "Status": status, + "StatusAr": statusAr, + }; +} + +class MedicalReportAttachment { + String? base64String; + String? fileName; + + MedicalReportAttachment({ + this.base64String, + this.fileName, + }); + + factory MedicalReportAttachment.fromRawJson(String str) => MedicalReportAttachment.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory MedicalReportAttachment.fromJson(Map json) => MedicalReportAttachment( + base64String: json["Base64String"], + fileName: json["FileName"], + ); + + Map toJson() => { + "Base64String": base64String, + "FileName": fileName, + }; +} + +class RelationshipType { + int? id; + String? text; + String? textAr; + String? textEn; + + RelationshipType({ + this.id, + this.text, + this.textAr, + this.textEn, + }); + + factory RelationshipType.fromRawJson(String str) => RelationshipType.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory RelationshipType.fromJson(Map json) => RelationshipType( + id: json["ID"], + text: json["Text"], + textAr: json["Text_Ar"], + textEn: json["Text_En"], + ); + + Map toJson() => { + "ID": id, + "Text": text, + "Text_Ar": textAr, + "Text_En": textEn, + }; +} diff --git a/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart b/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart new file mode 100644 index 0000000..bc93f59 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart @@ -0,0 +1,259 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class VitalSignResModel { + var transNo; + var projectID; + var weightKg; + var heightCm; + var temperatureCelcius; + var pulseBeatPerMinute; + var respirationBeatPerMinute; + var bloodPressureLower; + var bloodPressureHigher; + var sAO2; + var fIO2; + var painScore; + var bodyMassIndex; + var headCircumCm; + var leanBodyWeightLbs; + var idealBodyWeightLbs; + var temperatureCelciusMethod; + var pulseRhythm; + var respirationPattern; + var bloodPressureCuffLocation; + var bloodPressureCuffSize; + var bloodPressurePatientPosition; + var painLocation; + var painDuration; + var painCharacter; + var painFrequency; + bool? isPainManagementDone; + var status; + bool? isVitalsRequired; + var patientID; + var createdOn; + var doctorID; + var clinicID; + var triageCategory; + var gCScore; + var lineItemNo; + DateTime? vitalSignDate; + var actualTimeTaken; + var sugarLevel; + var fBS; + var rBS; + var observationType; + var heartRate; + var muscleTone; + var reflexIrritability; + var bodyColor; + var isFirstAssessment; + var dateofBirth; + var timeOfBirth; + var bloodPressure; + var bloodPressureCuffLocationDesc; + var bloodPressureCuffSizeDesc; + var bloodPressurePatientPositionDesc; + var clinicName; + var doctorImageURL; + var doctorName; + var painScoreDesc; + var pulseRhythmDesc; + var respirationPatternDesc; + var temperatureCelciusMethodDesc; + var time; + + VitalSignResModel( + {this.transNo, + this.projectID, + this.weightKg, + this.heightCm, + this.temperatureCelcius, + this.pulseBeatPerMinute, + this.respirationBeatPerMinute, + this.bloodPressureLower, + this.bloodPressureHigher, + this.sAO2, + this.fIO2, + this.painScore, + this.bodyMassIndex, + this.headCircumCm, + this.leanBodyWeightLbs, + this.idealBodyWeightLbs, + this.temperatureCelciusMethod, + this.pulseRhythm, + this.respirationPattern, + this.bloodPressureCuffLocation, + this.bloodPressureCuffSize, + this.bloodPressurePatientPosition, + this.painLocation, + this.painDuration, + this.painCharacter, + this.painFrequency, + this.isPainManagementDone, + this.status, + this.isVitalsRequired, + this.patientID, + this.createdOn, + this.doctorID, + this.clinicID, + this.triageCategory, + this.gCScore, + this.lineItemNo, + this.vitalSignDate, + this.actualTimeTaken, + this.sugarLevel, + this.fBS, + this.rBS, + this.observationType, + this.heartRate, + this.muscleTone, + this.reflexIrritability, + this.bodyColor, + this.isFirstAssessment, + this.dateofBirth, + this.timeOfBirth, + this.bloodPressure, + this.bloodPressureCuffLocationDesc, + this.bloodPressureCuffSizeDesc, + this.bloodPressurePatientPositionDesc, + this.clinicName, + this.doctorImageURL, + this.doctorName, + this.painScoreDesc, + this.pulseRhythmDesc, + this.respirationPatternDesc, + this.temperatureCelciusMethodDesc, + this.time}); + + VitalSignResModel.fromJson(Map json) { + transNo = json['TransNo']; + projectID = json['ProjectID']; + weightKg = json['WeightKg']; + heightCm = json['HeightCm']; + temperatureCelcius = json['TemperatureCelcius']; + pulseBeatPerMinute = json['PulseBeatPerMinute']; + respirationBeatPerMinute = json['RespirationBeatPerMinute']; + bloodPressureLower = json['BloodPressureLower']; + bloodPressureHigher = json['BloodPressureHigher']; + sAO2 = json['SAO2']; + fIO2 = json['FIO2']; + painScore = json['PainScore']; + bodyMassIndex = json['BodyMassIndex']; + headCircumCm = json['HeadCircumCm']; + leanBodyWeightLbs = json['LeanBodyWeightLbs']; + idealBodyWeightLbs = json['IdealBodyWeightLbs']; + temperatureCelciusMethod = json['TemperatureCelciusMethod']; + pulseRhythm = json['PulseRhythm']; + respirationPattern = json['RespirationPattern']; + bloodPressureCuffLocation = json['BloodPressureCuffLocation']; + bloodPressureCuffSize = json['BloodPressureCuffSize']; + bloodPressurePatientPosition = json['BloodPressurePatientPosition']; + painLocation = json['PainLocation']; + painDuration = json['PainDuration']; + painCharacter = json['PainCharacter']; + painFrequency = json['PainFrequency']; + isPainManagementDone = json['IsPainManagementDone']; + status = json['Status']; + isVitalsRequired = json['IsVitalsRequired']; + patientID = json['PatientID']; + createdOn = json['CreatedOn']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + triageCategory = json['TriageCategory']; + gCScore = json['GCScore']; + lineItemNo = json['LineItemNo']; + vitalSignDate = DateUtil.convertStringToDate(json['CreatedOn']); + actualTimeTaken = json['ActualTimeTaken']; + sugarLevel = json['SugarLevel']; + fBS = json['FBS']; + rBS = json['RBS']; + observationType = json['ObservationType']; + heartRate = json['HeartRate']; + muscleTone = json['MuscleTone']; + reflexIrritability = json['ReflexIrritability']; + bodyColor = json['BodyColor']; + isFirstAssessment = json['IsFirstAssessment']; + dateofBirth = json['DateofBirth']; + timeOfBirth = json['TimeOfBirth']; + bloodPressure = json['BloodPressure']; + bloodPressureCuffLocationDesc = json['BloodPressureCuffLocationDesc']; + bloodPressureCuffSizeDesc = json['BloodPressureCuffSizeDesc']; + bloodPressurePatientPositionDesc = json['BloodPressurePatientPositionDesc']; + clinicName = json['ClinicName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + painScoreDesc = json['PainScoreDesc']; + pulseRhythmDesc = json['PulseRhythmDesc']; + respirationPatternDesc = json['RespirationPatternDesc']; + temperatureCelciusMethodDesc = json['TemperatureCelciusMethodDesc']; + time = json['Time']; + } + + Map toJson() { + final Map data = new Map(); + data['TransNo'] = this.transNo; + data['ProjectID'] = this.projectID; + data['WeightKg'] = this.weightKg; + data['HeightCm'] = this.heightCm; + data['TemperatureCelcius'] = this.temperatureCelcius; + data['PulseBeatPerMinute'] = this.pulseBeatPerMinute; + data['RespirationBeatPerMinute'] = this.respirationBeatPerMinute; + data['BloodPressureLower'] = this.bloodPressureLower; + data['BloodPressureHigher'] = this.bloodPressureHigher; + data['SAO2'] = this.sAO2; + data['FIO2'] = this.fIO2; + data['PainScore'] = this.painScore; + data['BodyMassIndex'] = this.bodyMassIndex; + data['HeadCircumCm'] = this.headCircumCm; + data['LeanBodyWeightLbs'] = this.leanBodyWeightLbs; + data['IdealBodyWeightLbs'] = this.idealBodyWeightLbs; + data['TemperatureCelciusMethod'] = this.temperatureCelciusMethod; + data['PulseRhythm'] = this.pulseRhythm; + data['RespirationPattern'] = this.respirationPattern; + data['BloodPressureCuffLocation'] = this.bloodPressureCuffLocation; + data['BloodPressureCuffSize'] = this.bloodPressureCuffSize; + data['BloodPressurePatientPosition'] = this.bloodPressurePatientPosition; + data['PainLocation'] = this.painLocation; + data['PainDuration'] = this.painDuration; + data['PainCharacter'] = this.painCharacter; + data['PainFrequency'] = this.painFrequency; + data['IsPainManagementDone'] = this.isPainManagementDone; + data['Status'] = this.status; + data['IsVitalsRequired'] = this.isVitalsRequired; + data['PatientID'] = this.patientID; + data['CreatedOn'] = this.createdOn; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['TriageCategory'] = this.triageCategory; + data['GCScore'] = this.gCScore; + data['LineItemNo'] = this.lineItemNo; + data['VitalSignDate'] = this.vitalSignDate; + data['ActualTimeTaken'] = this.actualTimeTaken; + data['SugarLevel'] = this.sugarLevel; + data['FBS'] = this.fBS; + data['RBS'] = this.rBS; + data['ObservationType'] = this.observationType; + data['HeartRate'] = this.heartRate; + data['MuscleTone'] = this.muscleTone; + data['ReflexIrritability'] = this.reflexIrritability; + data['BodyColor'] = this.bodyColor; + data['IsFirstAssessment'] = this.isFirstAssessment; + data['DateofBirth'] = this.dateofBirth; + data['TimeOfBirth'] = this.timeOfBirth; + data['BloodPressure'] = this.bloodPressure; + data['BloodPressureCuffLocationDesc'] = this.bloodPressureCuffLocationDesc; + data['BloodPressureCuffSizeDesc'] = this.bloodPressureCuffSizeDesc; + data['BloodPressurePatientPositionDesc'] = + this.bloodPressurePatientPositionDesc; + data['ClinicName'] = this.clinicName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['PainScoreDesc'] = this.painScoreDesc; + data['PulseRhythmDesc'] = this.pulseRhythmDesc; + data['RespirationPatternDesc'] = this.respirationPatternDesc; + data['TemperatureCelciusMethodDesc'] = this.temperatureCelciusMethodDesc; + data['Time'] = this.time; + return data; + } +} diff --git a/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart b/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart new file mode 100644 index 0000000..35b0efe --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +class CovidQuestionnaireModel { + int? id; + String? questionEn; + String? questionAr; + int? ans; + + CovidQuestionnaireModel({ + this.id, + this.questionEn, + this.questionAr, + this.ans, + }); + + factory CovidQuestionnaireModel.fromRawJson(String str) => CovidQuestionnaireModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory CovidQuestionnaireModel.fromJson(Map json) => CovidQuestionnaireModel( + id: json["id"], + questionEn: json["questionEN"], + questionAr: json["questionAR"], + ans: json["ans"], + ); + + Map toJson() => { + "id": id, + "questionEN": questionEn, + "questionAR": questionAr, + "ans": ans, + }; +} diff --git a/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart b/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart new file mode 100644 index 0000000..8be00d6 --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart @@ -0,0 +1,47 @@ +// models/referral_models.dart +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/relationship_type_resp_mode.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; + +class ReferralFormData { + String requesterName = ''; + String requesterPhone = ''; + String searchPhone =''; + CountryEnum countryEnum = CountryEnum.saudiArabia; + CountryEnum patientCountryEnum = CountryEnum.saudiArabia; + + GetAllRelationshipTypeResponseModel? relationship; + String otherRelationshipName = ''; + + String patientIdentification = ''; + String patientName = ''; + String patientPhone = ''; + GetAllCitiesResponseModel? patientCity; + + List medicalReportImages = []; + HospitalsModel? branch; + bool isPatientInsured = false; + List insuredPatientImages = []; +} + +class FormValidationErrors { + String? requesterName; + String? requesterPhone; + String? relationship; + String? otherRelationshipName; + + String? patientIdentification; + String? patientName; + String? patientCity; + String? patientPhone; + + + String? medicalReport; + String? branch; + String? insuredDocument; + + String? searchValue; + String? searchPhone; +} \ No newline at end of file diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart new file mode 100644 index 0000000..7a92198 --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class HmgServicesComponentModel { + int action; + String title; + String subTitle; + String icon; + Color? iconColor; + bool isLogin; + bool isLocked; + Color bgColor; + String? route; + Function? onTap; + + HmgServicesComponentModel( + this.action, + this.title, + this.subTitle, + this.icon, + + this.isLogin, { + this.isLocked = false, + this.bgColor = Colors.white, + this.iconColor = Colors.white, + this.route, + this.onTap + }); +} diff --git a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart new file mode 100644 index 0000000..45b0ab6 --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// UI-only helper model for Vital Sign cards. +/// +/// Keeps presentation logic (chip colors, icon colors, simple status rules) +/// in one place so it can be reused across multiple pages. +class VitalSignUiModel { + final Color iconBg; + final Color iconFg; + final Color chipBg; + final Color chipFg; + + const VitalSignUiModel({ + required this.iconBg, + required this.iconFg, + required this.chipBg, + required this.chipFg, + }); + + /// Returns a color scheme for a card based on its [status] and [label]. + /// + /// Rules (mirrors existing behavior in Medical File page): + /// - Height is always blue. + /// - High => red scheme. + /// - Low => yellow scheme. + /// - Otherwise => green scheme (Normal). + static VitalSignUiModel scheme({required String? status, required String label}) { + final s = (status ?? '').toLowerCase(); + final l = label.toLowerCase(); + + // Height should always be blue. + if (l.contains('height')) { + return VitalSignUiModel( + iconBg: AppColors.infoColor.withValues(alpha: 0.12), + iconFg: AppColors.infoColor, + chipBg: AppColors.infoColor.withValues(alpha: 0.12), + chipFg: AppColors.infoColor, + ); + } + + if (s.contains('high')) { + return const VitalSignUiModel( + iconBg: AppColors.chipSecondaryLightRedColor, + iconFg: AppColors.primaryRedColor, + chipBg: AppColors.chipSecondaryLightRedColor, + chipFg: AppColors.primaryRedColor, + ); + } + + if (s.contains('low')) { + final Color yellowBg = AppColors.warningColor.withValues(alpha: 0.12); + return VitalSignUiModel( + iconBg: yellowBg, + iconFg: AppColors.warningColor, + chipBg: yellowBg, + chipFg: AppColors.warningColor, + ); + } + + // Normal (green) + final Color greenBg = AppColors.lightGreenColor; + return VitalSignUiModel( + iconBg: greenBg, + iconFg: AppColors.bgGreenColor, + chipBg: greenBg, + chipFg: AppColors.bgGreenColor, + ); + } + + /// Simple, user-friendly classification: + /// - Low: systolic < 90 OR diastolic < 60 + /// - High: systolic >= 140 OR diastolic >= 90 + /// - Normal: otherwise + /// Returns null if values are missing/unparseable. + static String? bloodPressureStatus({dynamic systolic, dynamic diastolic}) { + final int? s = toIntOrNull(systolic); + final int? d = toIntOrNull(diastolic); + if (s == null || d == null) return null; + + if (s < 90 || d < 60) return 'Low'; + if (s >= 140 || d >= 90) return 'High'; + return 'Normal'; + } + + static int? toIntOrNull(dynamic v) { + if (v == null) return null; + if (v is int) return v; + if (v is double) return v.round(); + return int.tryParse(v.toString()); + } + + static String bmiStatus(dynamic bmi) { + if (bmi == null) return 'N/A'; + final double bmiValue = double.tryParse(bmi.toString()) ?? 0; + if (bmiValue < 18.5) return 'Underweight'; + if (bmiValue < 25) return 'Normal'; + if (bmiValue < 30) return 'Overweight'; + return 'High'; + } +} + diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index 599bc39..1baa81b 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; @@ -36,6 +38,10 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { late AppState _appState; + Duration countdownDuration = Duration(minutes: 1, seconds: 0); + late ValueNotifier durationNotifier; + late Timer timer; + initImmediateLiveCare() { _appState = getIt(); immediateLiveCareClinicsList = []; @@ -144,7 +150,11 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { patientLiveCareHistoryList = apiResponse.data!; if (patientLiveCareHistoryList.isNotEmpty) { if (patientLiveCareHistoryList[0].callStatus! < 4) { + countdownDuration = Duration(seconds: 0); patientHasPendingLiveCareRequest = true; + countdownDuration = Duration(minutes: patientLiveCareHistoryList.first.watingtimeInteger!, seconds: 0); + durationNotifier = ValueNotifier(countdownDuration); + startTimer(); } else { patientHasPendingLiveCareRequest = false; } @@ -159,4 +169,20 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { }, ); } + + void startTimer() { + timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); + notifyListeners(); + } + + void addTime() { + final seconds = durationNotifier.value.inSeconds - 1; + if (seconds < 0) { + timer?.cancel(); + // Handle end of timer here + // showEndMessage(); + } else { + durationNotifier.value = Duration(seconds: seconds); + } + } } diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 8460bd4..0bbdda2 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patien import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_card_history.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_details_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_update_response_model.dart'; -import 'package:hmg_patient_app_new/features/lab/lab_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class InsuranceViewModel extends ChangeNotifier { @@ -78,10 +77,12 @@ class InsuranceViewModel extends ChangeNotifier { // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { isInsuranceLoading = false; + isInsuranceDataToBeLoaded = false; notifyListeners(); }, (apiResponse) { if (apiResponse.messageStatus == 2) { + isInsuranceDataToBeLoaded = false; // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { patientInsuranceList = apiResponse.data!; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 3bb793b..e661157 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -21,6 +21,7 @@ abstract class LabRepo { Future>> getLabResultReportPDF({required PatientLabOrdersResponseModel labOrder}); + Future>> getLabResultsByAppointmentNo({required num appointmentNo, required num projectID, required num clinicID}); } class LabRepoImp implements LabRepo { @@ -135,6 +136,7 @@ class LabRepoImp implements LabRepo { request['SetupID'] = laborder!.setupID; request['ProjectID'] = laborder.projectID; request['ClinicID'] = laborder.clinicID; + request['InvoiceType'] = laborder.invoiceType ?? ""; try { GenericApiModel>? apiResponse; Failure? failure; @@ -184,6 +186,7 @@ class LabRepoImp implements LabRepo { request['SetupID'] = laborder!.setupID; request['ProjectID'] = laborder.projectID; request['ClinicID'] = laborder.clinicID; + request['InvoiceType'] = laborder.invoiceType ?? ""; try { GenericApiModel>? apiResponse; Failure? failure; @@ -278,4 +281,41 @@ class LabRepoImp implements LabRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> getLabResultsByAppointmentNo({required num appointmentNo, required num projectID, required num clinicID}) async { + Map request = {}; + request['AppointmentNo'] = appointmentNo; + request['ProjectID'] = projectID; + request['ClinicID'] = clinicID; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_PATIENT_LAB_ORDERS_BY_APPOINTMENT, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response['ListLabResultsByAppNo'], + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 12f0f27..118c54a0 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -1,5 +1,5 @@ +import 'dart:collection'; import 'dart:core'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -9,13 +9,14 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/features/lab/lab_repo.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_details.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:intl/intl.dart' show DateFormat; -import 'package:logger/logger.dart'; class LabViewModel extends ChangeNotifier { bool isLabOrdersLoading = false; @@ -37,6 +38,13 @@ class LabViewModel extends ChangeNotifier { String labSpecialResult = ""; List labOrderTests = []; String patientLabResultReportPDFBase64 = ""; + String? flagForHighReferenceRange; + double highRefrenceValue = double.negativeInfinity; + double lowRefenceValue = double.infinity; + + double highTransformedReferenceValue = double.negativeInfinity; + double lowTransformedReferenceValue = double.infinity; + String? flagForLowReferenceRange; PatientLabOrdersResponseModel? currentlySelectedPatientOrder; @@ -65,24 +73,36 @@ class LabViewModel extends ChangeNotifier { List get labSuggestions => _labSuggestionsList; Set uniqueTests = {}; + List uniqueTestsList = []; + List indexedCharacterForUniqueTest = []; double maxY = 0.0; + double minY = double.infinity; double maxX = double.infinity; + // --- New grouping fields --- + bool isSortByClinic = true; + List> patientLabOrdersByClinic = []; + List> patientLabOrdersByHospital = []; + List> patientLabOrdersViewList = []; + LabViewModel( {required this.labRepo, required this.errorHandlerService, required this.navigationService}); initLabProvider() { - if (isLabNeedToLoad) { + // if (isLabNeedToLoad) { patientLabOrders.clear(); filteredLabOrders.clear(); labOrderTests.clear(); isLabOrdersLoading = true; isLabResultsLoading = true; + patientLabOrdersByClinic.clear(); + patientLabOrdersByHospital.clear(); + patientLabOrdersViewList.clear(); getPatientLabOrders(); - } + // } notifyListeners(); } @@ -91,14 +111,23 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + void setIsSortByClinic(bool value) { + isSortByClinic = value; + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; + notifyListeners(); + } + Future getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { - if (!isLabNeedToLoad) return; + // if (!isLabNeedToLoad) return; isLabOrdersLoading = true; patientLabOrders.clear(); uniqueTests.clear(); labOrderTests.clear(); uniqueTests = {}; + patientLabOrdersByClinic.clear(); + patientLabOrdersByHospital.clear(); + patientLabOrdersViewList.clear(); notifyListeners(); final result = await labRepo.getPatientLabOrders(); @@ -109,7 +138,7 @@ class LabViewModel extends ChangeNotifier { isLabResultsLoading = false; notifyListeners(); }, - // => await errorHandlerService.handleError(failure: failure), + // => await errorHandler_service.handleError(failure: failure), (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -119,6 +148,23 @@ class LabViewModel extends ChangeNotifier { tempLabOrdersList = apiResponse.data!; isLabOrdersLoading = false; isLabResultsLoading = false; + + // --- Build groups by clinic and by hospital (projectName) --- + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var order in patientLabOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + + patientLabOrdersByClinic = clinicMap.values.toList(); + patientLabOrdersByHospital = hospitalMap.values.toList(); + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; + filterSuggestions(); getUniqueTestDescription(); isLabNeedToLoad = false; @@ -158,7 +204,7 @@ class LabViewModel extends ChangeNotifier { filterSuggestions() { final List labels = patientLabOrders - .expand((order) => order.testDetails!) + .expand((order) => order.testDetails ?? []) .map((detail) => detail.description) .whereType() .toList(); @@ -193,16 +239,91 @@ class LabViewModel extends ChangeNotifier { createdOn: item.createdOn, model: item)) }; + uniqueTestsList = uniqueTests.toList(); + uniqueTestsList.sort((a, b) { + return a.description!.toLowerCase().compareTo(b.description!.toLowerCase()); + }); + indexedCharacterForUniqueTest.clear(); + for (var test in uniqueTestsList) { + String label = test.description ?? ""; + if (label.isEmpty) continue; + if (indexedCharacterForUniqueTest.contains(label[0].toLowerCase())) continue; + indexedCharacterForUniqueTest.add(label[0].toLowerCase()); + } + for (var element in uniqueTests) { labOrderTests.add(element.description ?? ""); } } + Future getLabResultsByAppointmentNo( + {required num appointmentNo, + required num projectID, + required num clinicID, + required int doctorID, + required String clinicName, + required String doctorName, + required String projectName, + required String appointmentDate, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + bool isVidaPlus = Utils.isVidaPlusProject(projectID.toInt()); + final result = await labRepo.getLabResultsByAppointmentNo(appointmentNo: appointmentNo, projectID: projectID, clinicID: clinicID); + + result.fold( + (failure) async { + // await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + if (onError != null) { + onError(apiResponse.errorMessage!); + } + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data != null && apiResponse.data!.isNotEmpty) { + PatientLabOrdersResponseModel labOrder = PatientLabOrdersResponseModel(); + + labOrder.invoiceNoVP = isVidaPlus ? apiResponse.data[0]['InvoiceNo'].toString() : "0"; + labOrder.invoiceNo = isVidaPlus ? "0" : apiResponse.data[0]['InvoiceNo'].toString(); + labOrder.orderNo = apiResponse.data[0]['OrderNo'].toString(); + labOrder.invoiceType = apiResponse.data[0]['InvoiceType'].toString(); + labOrder.setupID = apiResponse.data[0]['SetupID'].toString(); + labOrder.projectID = projectID.toString(); + labOrder.clinicID = clinicID.toInt(); + labOrder.doctorID = doctorID; + labOrder.clinicDescription = clinicName; + labOrder.doctorName = doctorName; + labOrder.projectName = projectName; + labOrder.orderDate = appointmentDate; + + currentlySelectedPatientOrder = labOrder; + + getPatientLabResultByHospital(labOrder); + getPatientSpecialResult(labOrder); + + if (onSuccess != null) { + onSuccess(apiResponse); + } + navigationService.push( + CustomPageRoute( + page: LabResultByClinic(labOrder: labOrder), + ), + ); + } else {} + notifyListeners(); + } + }, + ); + } + Future getPatientLabResultByHospital( PatientLabOrdersResponseModel laborder) async { isLabResultByHospitalLoading = true; notifyListeners(); - mainLabResultsByHospitals.clear; + mainLabResultsByHospitals.clear(); final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); @@ -224,12 +345,13 @@ class LabViewModel extends ChangeNotifier { ); } - Future getPatientLabResult(PatientLabOrdersResponseModel laborder, String procedureName, String testDescription) async { + Future getPatientLabResult(PatientLabOrdersResponseModel laborder, String procedureName, String testDescription, String unitOfMeasure) async { LoaderBottomSheet.showLoader(); mainLabResults.clear(); filteredGraphValues.clear(); maxY = double.negativeInfinity; - + minY = double.infinity; + maxX = double.infinity; final result = await labRepo.getPatientLabResults( laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")), @@ -244,11 +366,15 @@ class LabViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - var sortedResponse = sortByFlagAndValue(apiResponse.data!); - var recentThree = sort(sortedResponse); + var recentThree = sort(apiResponse.data!); mainLabResults = recentThree; - double counter = 1; + + double highRefrenceValue = double.negativeInfinity; + String? flagForHighReferenceRange; + double lowRefenceValue = double.infinity; + String? flagForLowReferenceRange; + recentThree.reversed.forEach((element) { try { var dateTime = @@ -259,6 +385,17 @@ class LabViewModel extends ChangeNotifier { maxY = resultValue; maxX = maxY; } + if (resultValue < minY) { + minY = resultValue; + } + if (highRefrenceValue < double.parse(element.referenceHigh ?? "0.0")) { + highRefrenceValue = double.parse(element.referenceHigh ?? "0.0"); + flagForHighReferenceRange = element.calculatedResultFlag; + } + if (lowRefenceValue > double.parse(element.referenceLow ?? "0.0")) { + lowRefenceValue = double.parse(element.referenceLow ?? "0.0"); + flagForLowReferenceRange = element.calculatedResultFlag; + } filteredGraphValues.add(DataPoint( value: transformedValue, @@ -270,10 +407,27 @@ class LabViewModel extends ChangeNotifier { referenceValue: element.calculatedResultFlag ?? "", )); - counter++; } catch (e) {} }); + if (flagForLowReferenceRange == null && flagForHighReferenceRange == null) { + highRefrenceValue = maxY; + lowRefenceValue = minY; + } + + this.flagForHighReferenceRange = flagForHighReferenceRange; + this.flagForLowReferenceRange = flagForLowReferenceRange; + highTransformedReferenceValue = double.parse(transformValueInRange(highRefrenceValue, flagForHighReferenceRange ?? "").toStringAsFixed(1)); + lowTransformedReferenceValue = double.parse(transformValueInRange(lowRefenceValue, flagForLowReferenceRange ?? "").toStringAsFixed(1)); + this.highRefrenceValue = double.parse(highRefrenceValue.toStringAsFixed(1)); + this.lowRefenceValue = double.parse(lowRefenceValue.toStringAsFixed(1)); + + if(maxY< highRefrenceValue) { + maxY = highRefrenceValue; + } + maxY += 25; + minY -= 25; LabResult recentResult = recentThree.first; + recentResult.uOM = unitOfMeasure; checkIfGraphShouldBeDisplayed(recentResult); recentResult.verifiedOn = resultDate(DateUtil.convertStringToDate(recentResult.verifiedOnDateTime!)); // filteredGraphValues = [filteredGraphValues.first]; @@ -494,19 +648,6 @@ class LabViewModel extends ChangeNotifier { } List sort(List original) { - DateTime? parseVerifiedDate(String? raw) { - if (raw == null) return null; - final regex = RegExp(r'\/Date\((\d+)\)\/'); - final match = regex.firstMatch(raw); - if (match != null) { - final millis = int.tryParse(match.group(1)!); - if (millis != null) { - - return DateTime.fromMillisecondsSinceEpoch(millis); - } - } - return null; - } final copy = List.from(original); copy.sort((a, b) { @@ -526,8 +667,8 @@ class LabViewModel extends ChangeNotifier { final copy = List.from(original); copy.sort((a, b) { - final aDate =a.time; - final bDate = a.time; + final aDate = a.time; + final bDate = b.time; final now = DateTime.now(); if (aDate == now && bDate == now) return 0; if (aDate == now) return 1; diff --git a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart index 8bef76a..1c437a4 100644 --- a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart +++ b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart @@ -250,4 +250,7 @@ class TestDetails { data['CreatedOn'] = this.createdOn; return data; } + + @override + String toString() { return description??"";} } diff --git a/lib/features/location/location_view_model.dart b/lib/features/location/location_view_model.dart index e2e0cc5..5b0eef6 100644 --- a/lib/features/location/location_view_model.dart +++ b/lib/features/location/location_view_model.dart @@ -2,15 +2,14 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show ChangeNotifier; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter_platform_interface/src/types/camera.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:huawei_map/huawei_map.dart' as HMSCameraServices; -import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; - - import 'PlacePrediction.dart'; @@ -18,26 +17,49 @@ class LocationViewModel extends ChangeNotifier { final LocationRepo locationRepo; final ErrorHandlerService errorHandlerService; - LocationViewModel({required this.locationRepo, required this.errorHandlerService}); + LocationViewModel({required this.locationRepo, required this.errorHandlerService}) { + placeValueInController(); + } - List predictions = []; - PlacePrediction? selectedPrediction; + List predictions = []; + PlacePrediction? selectedPrediction; bool isPredictionLoading = false; GeocodeResponse? geocodeResponse; PlaceDetails? placeDetails; Location? mapCapturedLocation; + Completer? gmsController; + Completer? hmsController; + + HMSCameraServices.CameraPosition getHMSLocation() { + return HMSCameraServices.CameraPosition(target: HMSCameraServices.LatLng(getIt().userLat, getIt().userLong), zoom: 18); + } + + GMSMapServices.CameraPosition getGMSLocation() { + return GMSMapServices.CameraPosition( + target: GMSMapServices.LatLng(getIt().userLat != 0.0 ? getIt().userLat : 24.7248316, getIt().userLong != 0.0 ? getIt().userLong : 46.4928828), + zoom: 18); + } + + void placeValueInController() async { + if (await getIt().isGMSAvailable) { + gmsController = Completer(); + } else { + hmsController = Completer(); + } + } + FutureOr getPlacesPrediction(String input) async { predictions = []; - isPredictionLoading= true; + isPredictionLoading = true; final result = await locationRepo.getPlacePredictionsAsInput(input); result.fold( (failure) { errorHandlerService.handleError(failure: failure); }, (apiModel) { - predictions = apiModel.data??[]; + predictions = apiModel.data ?? []; }, ); isPredictionLoading = false; @@ -75,21 +97,20 @@ class LocationViewModel extends ChangeNotifier { handleGMSMapCameraMoved(GMSMapServices.CameraPosition value) { mapCapturedLocation = Location(lat: value.target.latitude, lng: value.target.longitude); - } handleHMSMapCameraMoved(HMSCameraServices.CameraPosition value) { mapCapturedLocation = Location(lat: value.target.lat, lng: value.target.lng); } - handleOnCameraIdle(){ - if(mapCapturedLocation != null) { + handleOnCameraIdle() { + if (mapCapturedLocation != null) { getPlaceEncodedData(mapCapturedLocation!.lat, mapCapturedLocation!.lng); } } void updateSearchQuery(String? value) { - if(value == null || value.isEmpty){ + if (value == null || value.isEmpty) { predictions = []; return; } @@ -99,18 +120,48 @@ class LocationViewModel extends ChangeNotifier { void flushSearchPredictions() { predictions = []; - mapCapturedLocation= null; - placeDetails= null; - geocodeResponse= null; - selectedPrediction= null; + mapCapturedLocation = null; + placeDetails = null; + geocodeResponse = null; + selectedPrediction = null; notifyListeners(); } - FutureOr selectPlacePrediction(PlacePrediction placePrediction) async{ - selectedPrediction= placePrediction; + FutureOr selectPlacePrediction(PlacePrediction placePrediction) async { + selectedPrediction = placePrediction; await getPlaceDetails(placePrediction.placeID); } + void moveToCurrentLocation() { + moveController(Location(lat: getIt().userLat, lng: getIt().userLong)); + } -} \ No newline at end of file + void moveController(Location location) { + if (getIt().isGMSAvailable) { + gmsController?.future.then((controller) { + controller.animateCamera( + GMSMapServices.CameraUpdate.newCameraPosition( + GMSMapServices.CameraPosition( + target: GMSMapServices.LatLng(location.lat, location.lng), + zoom: 18, + ), + ), + ); + }); + } else { + print("hmsController is null or not $hmsController"); + + hmsController?.future.then((controller) { + controller.animateCamera( + HMSCameraServices.CameraUpdate.newCameraPosition( + HMSCameraServices.CameraPosition( + target: HMSCameraServices.LatLng(location.lat, location.lng), + zoom: 18, + ), + ), + ); + }); + } + } +} diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index ab09ca6..2f5cae6 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -3,8 +3,8 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_allergies_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_model.dart'; @@ -38,6 +38,8 @@ abstract class MedicalFileRepo { Future>> removeFamilyFile({required int? id}); Future>> acceptRejectFamilyFile({required int? id, required int? status}); + + Future>>> getPatientAllergiesList({Function(dynamic)? onSuccess, Function(String)? onError}); } class MedicalFileRepoImp implements MedicalFileRepo { @@ -549,4 +551,42 @@ class MedicalFileRepoImp implements MedicalFileRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientAllergiesList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = {"isDentalAllowedBackend": false, "OutSA": 0}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_PATIENT_ALLERGIES, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['Patient_Allergies']; + + final vaccinesList = list.map((item) => GetAllergiesResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: vaccinesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 4b5a14c..d67e5de 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.dart'; @@ -9,6 +8,7 @@ import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_allergies_response_model.dart'; import 'package:hmg_patient_app_new/features/common/models/family_file_request.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; @@ -27,6 +27,7 @@ class MedicalFileViewModel extends ChangeNotifier { bool isPatientSickLeaveListLoading = false; bool isPatientSickLeavePDFLoading = false; bool isPatientMedicalReportsListLoading = false; + bool isPatientAllergiesListLoading = false; MedicalFileRepo medicalFileRepo; ErrorHandlerService errorHandlerService; @@ -34,12 +35,26 @@ class MedicalFileViewModel extends ChangeNotifier { List patientVaccineList = []; List patientSickLeaveList = []; + List patientSickLeavesByClinic = []; + List patientSickLeavesByHospital = []; + List patientSickLeavesViewList = []; + + bool isSickLeavesSortByClinic = true; + + List patientAllergiesList = []; + List patientMedicalReportList = []; List patientMedicalReportRequestedList = []; List patientMedicalReportReadyList = []; List patientMedicalReportCancelledList = []; + List patientMedicalReportsByClinic = []; + List patientMedicalReportsByHospital = []; + List patientMedicalReportsViewList = []; + + bool isMedicalReportsSortByClinic = true; + List patientMedicalReportAppointmentHistoryList = []; PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; @@ -69,8 +84,10 @@ class MedicalFileViewModel extends ChangeNotifier { initMedicalFileProvider() { patientMedicalReportAppointmentHistoryList.clear(); + patientAllergiesList.clear(); isPatientVaccineListLoading = true; isPatientMedicalReportsListLoading = true; + isPatientAllergiesListLoading = true; notifyListeners(); } @@ -84,12 +101,54 @@ class MedicalFileViewModel extends ChangeNotifier { } else if (index == 2) { patientMedicalReportList = patientMedicalReportCancelledList; } + // Re-group by clinic/hospital for the selected tab + _groupMedicalReportsByClinicAndHospital(); + notifyListeners(); + } + + void _groupMedicalReportsByClinicAndHospital() { + patientMedicalReportsByClinic.clear(); + patientMedicalReportsByHospital.clear(); + + for (var element in patientMedicalReportList) { + // Group by clinic + List reportsByClinic = patientMedicalReportsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicDescription).toList(); + + if (reportsByClinic.isNotEmpty) { + patientMedicalReportsByClinic[patientMedicalReportsByClinic.indexOf(reportsByClinic[0])].medicalReportsList!.add(element); + } else { + patientMedicalReportsByClinic.add(MedicalReportList(filterName: element.clinicDescription, medicalReport: element)); + } + + // Group by hospital + List reportsByHospital = patientMedicalReportsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (reportsByHospital.isNotEmpty) { + patientMedicalReportsByHospital[patientMedicalReportsByHospital.indexOf(reportsByHospital[0])].medicalReportsList!.add(element); + } else { + patientMedicalReportsByHospital.add(MedicalReportList(filterName: element.projectName, medicalReport: element)); + } + } + + if (isMedicalReportsSortByClinic) { + patientMedicalReportsViewList = patientMedicalReportsByClinic; + } else { + patientMedicalReportsViewList = patientMedicalReportsByHospital; + } + } + + setIsMedicalReportsSortByClinic(bool value) { + isMedicalReportsSortByClinic = value; + if (isMedicalReportsSortByClinic) { + patientMedicalReportsViewList = patientMedicalReportsByClinic; + } else { + patientMedicalReportsViewList = patientMedicalReportsByHospital; + } notifyListeners(); } void onFamilyFileTabChange(int index) { setSelectedFamilyFileTabIndex = index; - notifyListeners(); } setIsPatientVaccineListLoading(bool isLoading) { @@ -105,17 +164,35 @@ class MedicalFileViewModel extends ChangeNotifier { setIsPatientSickLeaveListLoading(bool val) { if (val) { patientSickLeaveList.clear(); + patientSickLeavesByClinic.clear(); + patientSickLeavesByHospital.clear(); + patientSickLeavesViewList.clear(); patientSickLeavePDFBase64 = ""; + isSickLeavesSortByClinic = true; } isPatientSickLeaveListLoading = val; notifyListeners(); } + setIsSickLeavesSortByClinic(bool value) { + isSickLeavesSortByClinic = value; + if (isSickLeavesSortByClinic) { + patientSickLeavesViewList = patientSickLeavesByClinic; + } else { + patientSickLeavesViewList = patientSickLeavesByHospital; + } + notifyListeners(); + } + setIsPatientMedicalReportsLoading(bool val) { if (val) { onMedicalReportTabChange(0); patientMedicalReportList.clear(); + patientMedicalReportsByClinic.clear(); + patientMedicalReportsByHospital.clear(); + patientMedicalReportsViewList.clear(); patientMedicalReportPDFBase64 = ""; + isMedicalReportsSortByClinic = true; } isPatientMedicalReportsListLoading = val; notifyListeners(); @@ -162,6 +239,32 @@ class MedicalFileViewModel extends ChangeNotifier { ); } + Future getPatientAllergiesList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isPatientAllergiesListLoading = true; + patientAllergiesList.clear(); + notifyListeners(); + final result = await medicalFileRepo.getPatientAllergiesList(); + + result.fold( + (failure) async { + isPatientAllergiesListLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientAllergiesList = apiResponse.data!; + isPatientAllergiesListLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async { patientSickLeaveList.clear(); final result = await medicalFileRepo.getPatientSickLeavesList(); @@ -183,6 +286,29 @@ class MedicalFileViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientSickLeaveList = apiResponse.data!; isPatientSickLeaveListLoading = false; + + // Group by clinic and hospital + for (var element in patientSickLeaveList) { + // Group by clinic + List sickLeavesByClinic = patientSickLeavesByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList(); + + if (sickLeavesByClinic.isNotEmpty) { + patientSickLeavesByClinic[patientSickLeavesByClinic.indexOf(sickLeavesByClinic[0])].sickLeavesList!.add(element); + } else { + patientSickLeavesByClinic.add(SickLeaveList(filterName: element.clinicName, sickLeaves: element)); + } + + // Group by hospital + List sickLeavesByHospital = patientSickLeavesByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (sickLeavesByHospital.isNotEmpty) { + patientSickLeavesByHospital[patientSickLeavesByHospital.indexOf(sickLeavesByHospital[0])].sickLeavesList!.add(element); + } else { + patientSickLeavesByHospital.add(SickLeaveList(filterName: element.projectName, sickLeaves: element)); + } + } + patientSickLeavesViewList = patientSickLeavesByClinic; + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -442,9 +568,6 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - - - Future addFamilyFile({required OTPTypeEnum otpTypeEnum}) async { LoaderBottomSheet.showLoader(); AuthenticationViewModel authVM = getIt.get(); @@ -489,6 +612,13 @@ class MedicalFileViewModel extends ChangeNotifier { }); } + Future clearAuthValues() async { + authVM.nationalIdController.clear(); + authVM.phoneNumberController.clear(); + authVM.emailController.clear(); + authVM.dobController.clear(); + } + Future handleFamilyFileRequestOTPVerification() async { LoaderBottomSheet.showLoader(); if (!_appState.getIsChildLoggedIn) { @@ -600,7 +730,7 @@ class MedicalFileViewModel extends ChangeNotifier { getFamilyFiles(status: 0); getAllPendingRecordsByResponseId(); LoaderBottomSheet.hideLoader(); - onFamilyFileTabChange(0); + // onFamilyFileTabChange(0); } }, ); diff --git a/lib/features/medical_file/models/patient_medical_response_model.dart b/lib/features/medical_file/models/patient_medical_response_model.dart index 52785af..3bcf5cd 100644 --- a/lib/features/medical_file/models/patient_medical_response_model.dart +++ b/lib/features/medical_file/models/patient_medical_response_model.dart @@ -190,3 +190,16 @@ class PatientMedicalReportResponseModel { return data; } } + +class MedicalReportList { + String? filterName; + List? medicalReportsList; + + MedicalReportList({this.filterName, PatientMedicalReportResponseModel? medicalReport}) { + medicalReportsList = []; + if (medicalReport != null) { + medicalReportsList!.add(medicalReport); + } + } +} + diff --git a/lib/features/medical_file/models/patient_sickleave_response_model.dart b/lib/features/medical_file/models/patient_sickleave_response_model.dart index 3bf732c..53c886f 100644 --- a/lib/features/medical_file/models/patient_sickleave_response_model.dart +++ b/lib/features/medical_file/models/patient_sickleave_response_model.dart @@ -174,3 +174,16 @@ class PatientSickLeavesResponseModel { return data; } } + +class SickLeaveList { + String? filterName; + List? sickLeavesList; + + SickLeaveList({this.filterName, PatientSickLeavesResponseModel? sickLeaves}) { + sickLeavesList = []; + if (sickLeaves != null) { + sickLeavesList!.add(sickLeaves); + } + } +} + diff --git a/lib/features/monthly_report/monthly_report_repo.dart b/lib/features/monthly_report/monthly_report_repo.dart new file mode 100644 index 0000000..429700c --- /dev/null +++ b/lib/features/monthly_report/monthly_report_repo.dart @@ -0,0 +1,53 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class MonthlyReportRepo { + Future>> updatePatientHealthSummaryReport({required bool rSummaryReport}); +} + +class MonthlyReportRepoImp implements MonthlyReportRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MonthlyReportRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>> updatePatientHealthSummaryReport({required bool rSummaryReport}) async { + Map mapDevice = { + "RSummaryReport": rSummaryReport, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + UPDATE_HEALTH_TERMS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/monthly_report/monthly_report_view_model.dart b/lib/features/monthly_report/monthly_report_view_model.dart new file mode 100644 index 0000000..348ca92 --- /dev/null +++ b/lib/features/monthly_report/monthly_report_view_model.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class MonthlyReportViewModel extends ChangeNotifier { + MonthlyReportRepo monthlyReportRepo; + ErrorHandlerService errorHandlerService; + + bool isUpdateHealthSummaryLoading = false; + bool isHealthSummaryEnabled = false; + + MonthlyReportViewModel({ + required this.monthlyReportRepo, + required this.errorHandlerService, + }); + + setHealthSummaryEnabled(bool value) { + isHealthSummaryEnabled = value; + notifyListeners(); + } + + Future updatePatientHealthSummaryReport({ + required bool rSummaryReport, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isUpdateHealthSummaryLoading = true; + notifyListeners(); + + final result = await monthlyReportRepo.updatePatientHealthSummaryReport( + rSummaryReport: rSummaryReport, + ); + + result.fold( + (failure) async { + isUpdateHealthSummaryLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdateHealthSummaryLoading = false; + if (apiResponse.messageStatus == 2) { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Unknown error"); + } + } else if (apiResponse.messageStatus == 1) { + // Update the local state on success + isHealthSummaryEnabled = rSummaryReport; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/features/my_appointments/appointment_rating_view_model.dart b/lib/features/my_appointments/appointment_rating_view_model.dart new file mode 100644 index 0000000..6dd41ff --- /dev/null +++ b/lib/features/my_appointments/appointment_rating_view_model.dart @@ -0,0 +1,133 @@ +// dart +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +import 'models/resp_models/rate_appointment_resp_model.dart'; + +class AppointmentRatingViewModel extends ChangeNotifier { + final MyAppointmentsRepo myAppointmentsRepo; + final ErrorHandlerService errorHandlerService; + final AppState appState; + List appointmentRatedList = []; + AppointmentDetails? appointmentDetails; + AppointmentRatingViewModel({ + required this.myAppointmentsRepo, + required this.errorHandlerService, + required this.appState, + }); + + + String title = ""; + String subTitle = ""; + bool isRateClinic = false; + + Future getLastRatingAppointment({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getLastRatingAppointment(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentRatedList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future getAppointmentDetails(int appointmentID, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getAppointmentDetails(appointmentID, projectID); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentDetails = apiResponse.data ?? AppointmentDetails(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + Future submitDoctorRating( {required int docRate, required String docNote,Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendDoctorRate( + docRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + docNote, + appointmentDetails!.appointmentDate!, + appointmentDetails!.doctorName, + appointmentDetails!.projectName, + appointmentDetails!.clinicName + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future submitClinicRating( { required int clinicRate, required String clinicNote, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendAppointmentRate( + clinicRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + clinicNote + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + void setSubTitle(String value) { + this.subTitle = value; + notifyListeners(); + } + + void setTitle(String value) { + this.title = value; + notifyListeners(); + } + void setClinicOrDoctor(bool value){ + this.isRateClinic = value; + notifyListeners(); + } +} diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 6c6354a..c5dcaf6 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -1,13 +1,16 @@ import 'package:flutter/foundation.dart' show ChangeNotifier; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart' show AppState; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; enum AppointmentViaRegionState { REGION_SELECTION, @@ -31,7 +34,14 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { AppointmentViaRegionState bottomSheetState = AppointmentViaRegionState.REGION_SELECTION; final AppState appState; - + TextEditingController searchController = TextEditingController(); + List? hospitalList; + List? hmgHospitalList; + List? hmcHospitalList; + List? displayList; + FacilitySelection selectedFacility = FacilitySelection.ALL; + int hmgCount = 0; + int hmcCount = 0; RegionBottomSheetType regionBottomSheetType = RegionBottomSheetType.FOR_REGION; AppointmentViaRegionViewmodel({required this.navigationService,required this.appState}); @@ -41,6 +51,35 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { notifyListeners(); } + void setDisplayListAndRegionHospitalList(PatientDoctorAppointmentListByRegion? registeredDoctorMap){ + if(registeredDoctorMap == null) { + return; + } + selectedFacility = FacilitySelection.ALL; + hmcHospitalList = []; + hmgHospitalList = []; + hospitalList = []; + displayList = []; + for(var data in registeredDoctorMap.hmgDoctorList!){ + hmgHospitalList?.add(data); + + } + + for(var data in registeredDoctorMap.hmcDoctorList!){ + hmcHospitalList?.add(data); + + } + + hospitalList!.addAll(hmgHospitalList!); + hospitalList!.addAll(hmcHospitalList!); + + hmcCount = registeredDoctorMap.hmcSize; + hmgCount = registeredDoctorMap.hmgSize; + + getDisplayList(); + + } + void setFacility(String? facility) { selectedFacilityType = facility; notifyListeners(); @@ -72,7 +111,7 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { setSelectedRegionId(null); break; case AppointmentViaRegionState.HOSPITAL_SELECTION: - setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); + setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); break; default: } @@ -122,7 +161,7 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { } void handleLastStepForDentalAndLaser() { - //todo handle the routing here + //todo_section handle the routing here navigationService.pop(); navigationService.push( CustomPageRoute( @@ -130,4 +169,48 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { ), ); } + + searchHospitals(String query) { + if (query.isEmpty) { + getDisplayList(); + return; + } + List? sourceList; + switch (selectedFacility) { + case FacilitySelection.ALL: + sourceList = hospitalList; + break; + case FacilitySelection.HMG: + sourceList = hmgHospitalList; + break; + case FacilitySelection.HMC: + sourceList = hmcHospitalList; + break; + } + displayList = sourceList?.where((hospital) => hospital.filterName != null && hospital.filterName!.toLowerCase().contains(query.toLowerCase())).toList(); + notifyListeners(); + } + + getDisplayList() { + switch (selectedFacility) { + case FacilitySelection.ALL: + displayList = hospitalList; + break; + case FacilitySelection.HMG: + displayList = hmgHospitalList; + break; + case FacilitySelection.HMC: + displayList = hmcHospitalList; + break; + } + notifyListeners(); + } + + + setSelectedFacility(FacilitySelection selection) { + selectedFacility = selection; + notifyListeners(); + } + + } diff --git a/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart new file mode 100644 index 0000000..91070f2 --- /dev/null +++ b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart @@ -0,0 +1,100 @@ +class AppointmentRate { + int? rate; + int? appointmentNo; + int? projectID; + int? doctorID; + int? clinicID; + String? note; + String? mobileNumber; + int? createdBy; + int? editedBy; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + + AppointmentRate( + {this.rate, + this.appointmentNo, + this.projectID, + this.doctorID, + this.clinicID, + this.note, + this.mobileNumber, + this.createdBy, + this.editedBy, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType}); + + AppointmentRate.fromJson(Map json) { + rate = json['Rate']; + appointmentNo = json['AppointmentNo']; + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + note = json['Note']; + mobileNumber = json['MobileNumber']; + createdBy = json['CreatedBy']; + editedBy = json['EditedBy']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + } + + Map toJson() { + final Map data = new Map(); + data['Rate'] = this.rate; + data['AppointmentNo'] = this.appointmentNo; + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['Note'] = this.note; + data['MobileNumber'] = this.mobileNumber; + data['CreatedBy'] = this.createdBy; + data['EditedBy'] = this.editedBy; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientID'] = this.patientID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart new file mode 100644 index 0000000..2900bce --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart @@ -0,0 +1,64 @@ +class AppointmentDetails { + String? setupID; + int? projectID; + int? patientID; + int? appointmentNo; + int? clinicID; + int? doctorID; + dynamic startTime; + dynamic endTime; + dynamic appointmentDate; + dynamic clinicName; + dynamic doctorImageURL; + dynamic doctorName; + dynamic projectName; + + AppointmentDetails( + {this.setupID, + this.projectID, + this.patientID, + this.appointmentNo, + this.clinicID, + this.doctorID, + this.startTime, + this.endTime, + this.appointmentDate, + this.clinicName, + this.doctorImageURL, + this.doctorName, + this.projectName}); + + AppointmentDetails.fromJson(Map json) { + setupID = json['SetupID']; + projectID = json['ProjectID']; + patientID = json['PatientID']; + appointmentNo = json['AppointmentNo']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + startTime = json['StartTime']; + endTime = json['EndTime']; + appointmentDate = json['AppointmentDate']; + clinicName = json['ClinicName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + projectName = json['ProjectName']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['PatientID'] = this.patientID; + data['AppointmentNo'] = this.appointmentNo; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['AppointmentDate'] = this.appointmentDate; + data['ClinicName'] = this.clinicName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['ProjectName'] = this.projectName; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart b/lib/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart new file mode 100644 index 0000000..3b53214 --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart @@ -0,0 +1,92 @@ +class AskDocRequestType { + String? setupID; + int? parameterGroup; + int? parameterType; + int? parameterCode; + String? description; + String? descriptionN; + String? alias; + String? aliasN; + String? prefix; + String? suffix; + String? isColorCodingRequired; + String? backColor; + String? foreColor; + bool? isBuiltIn; + bool? isActive; + int? createdBy; + String? createdOn; + String? editedBy; + String? editedOn; + String? rowVer; + + AskDocRequestType( + {this.setupID, + this.parameterGroup, + this.parameterType, + this.parameterCode, + this.description, + this.descriptionN, + this.alias, + this.aliasN, + this.prefix, + this.suffix, + this.isColorCodingRequired, + this.backColor, + this.foreColor, + this.isBuiltIn, + this.isActive, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.rowVer}); + + AskDocRequestType.fromJson(Map json) { + setupID = json['SetupID']; + parameterGroup = json['ParameterGroup']; + parameterType = json['ParameterType']; + parameterCode = json['ParameterCode']; + description = json['Description']; + descriptionN = json['DescriptionN']; + alias = json['Alias']; + aliasN = json['AliasN']; + prefix = json['Prefix']; + suffix = json['Suffix']; + isColorCodingRequired = json['IsColorCodingRequired']; + backColor = json['BackColor']; + foreColor = json['ForeColor']; + isBuiltIn = json['IsBuiltIn']; + isActive = json['IsActive']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + rowVer = json['RowVer']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ParameterGroup'] = this.parameterGroup; + data['ParameterType'] = this.parameterType; + data['ParameterCode'] = this.parameterCode; + data['Description'] = this.description; + data['DescriptionN'] = this.descriptionN; + data['Alias'] = this.alias; + data['AliasN'] = this.aliasN; + data['Prefix'] = this.prefix; + data['Suffix'] = this.suffix; + data['IsColorCodingRequired'] = this.isColorCodingRequired; + data['BackColor'] = this.backColor; + data['ForeColor'] = this.foreColor; + data['IsBuiltIn'] = this.isBuiltIn; + data['IsActive'] = this.isActive; + data['CreatedBy'] = this.createdBy; + data['CreatedOn'] = this.createdOn; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['RowVer'] = this.rowVer; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/hospital_model.dart b/lib/features/my_appointments/models/resp_models/hospital_model.dart index 9a211d0..a807b99 100644 --- a/lib/features/my_appointments/models/resp_models/hospital_model.dart +++ b/lib/features/my_appointments/models/resp_models/hospital_model.dart @@ -62,9 +62,9 @@ class HospitalsModel { mainProjectID = json['MainProjectID']; projectOutSA = json['ProjectOutSA']; usingInDoctorApp = json['UsingInDoctorApp']; - this.isHMC = json["IsHMC"]; - this.regionArabic = json['RegionNameN']; - this.regionEnglish = json['RegionName']; + isHMC = json["IsHMC"]; + regionArabic = json['RegionNameN']; + regionEnglish = json['RegionName']; } String? getRegionName(bool isArabic) { @@ -83,24 +83,22 @@ class HospitalsModel { Map toJson() { final Map data = new Map(); - data['Desciption'] = this.desciption; - data['DesciptionN'] = this.desciptionN; - data['ID'] = this.iD; - data['LegalName'] = this.legalName; - data['LegalNameN'] = this.legalNameN; - data['Name'] = this.name; - data['NameN'] = this.nameN; - data['PhoneNumber'] = this.phoneNumber; - data['SetupID'] = this.setupID; - data['DistanceInKilometers'] = this.distanceInKilometers; - data['IsActive'] = this.isActive; - data['Latitude'] = this.latitude; - data['Longitude'] = this.longitude; - data['MainProjectID'] = this.mainProjectID; - data['ProjectOutSA'] = this.projectOutSA; - data['UsingInDoctorApp'] = this.usingInDoctorApp; + data['Desciption'] = desciption; + data['DesciptionN'] = desciptionN; + data['ID'] = iD; + data['LegalName'] = legalName; + data['LegalNameN'] = legalNameN; + data['Name'] = name; + data['NameN'] = nameN; + data['PhoneNumber'] = phoneNumber; + data['SetupID'] = setupID; + data['DistanceInKilometers'] = distanceInKilometers; + data['IsActive'] = isActive; + data['Latitude'] = latitude; + data['Longitude'] = longitude; + data['MainProjectID'] = mainProjectID; + data['ProjectOutSA'] = projectOutSA; + data['UsingInDoctorApp'] = usingInDoctorApp; return data; } - - } diff --git a/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart b/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart index 4d8eb9c..e1ae67d 100644 --- a/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart +++ b/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart @@ -55,8 +55,8 @@ class PatientAppointmentHistoryResponseModel { bool? isMedicalReportRequested; bool? isOnlineCheckedIN; String? latitude; - dynamic listHISGetContactLensPerscription; - dynamic listHISGetGlassPerscription; + List? listHISGetContactLensPrescription; + List? listHISGetGlassPrescription; String? longitude; dynamic nextAction; dynamic noOfPatientsRate; @@ -130,8 +130,8 @@ class PatientAppointmentHistoryResponseModel { this.isMedicalReportRequested, this.isOnlineCheckedIN, this.latitude, - this.listHISGetContactLensPerscription, - this.listHISGetGlassPerscription, + this.listHISGetContactLensPrescription, + this.listHISGetGlassPrescription, this.longitude, this.nextAction, this.noOfPatientsRate, @@ -207,8 +207,18 @@ class PatientAppointmentHistoryResponseModel { isMedicalReportRequested = json['IsMedicalReportRequested']; isOnlineCheckedIN = json['IsOnlineCheckedIN']; latitude = json['Latitude']; - listHISGetContactLensPerscription = json['List_HIS_GetContactLensPerscription']; - listHISGetGlassPerscription = json['List_HIS_GetGlassPerscription']; + if (json['List_HIS_GetContactLensPerscription'] != null) { + listHISGetContactLensPrescription = []; + json['List_HIS_GetContactLensPerscription'].forEach((v) { + listHISGetContactLensPrescription!.add(ListHISGetContactLensPrescription.fromJson(v)); + }); + } + if (json['List_HIS_GetGlassPerscription'] != null) { + listHISGetGlassPrescription = []; + json['List_HIS_GetGlassPerscription'].forEach((v) { + listHISGetGlassPrescription!.add(ListHISGetGlassPrescription.fromJson(v)); + }); + } longitude = json['Longitude']; nextAction = json['NextAction']; noOfPatientsRate = json['NoOfPatientsRate']; @@ -285,8 +295,8 @@ class PatientAppointmentHistoryResponseModel { data['IsMedicalReportRequested'] = this.isMedicalReportRequested; data['IsOnlineCheckedIN'] = this.isOnlineCheckedIN; data['Latitude'] = this.latitude; - data['List_HIS_GetContactLensPerscription'] = this.listHISGetContactLensPerscription; - data['List_HIS_GetGlassPerscription'] = this.listHISGetGlassPerscription; + // data['List_HIS_GetContactLensPerscription'] = this.listHISGetContactLensPerscription; + // data['List_HIS_GetGlassPerscription'] = this.listHISGetGlassPerscription; data['Longitude'] = this.longitude; data['NextAction'] = this.nextAction; data['NoOfPatientsRate'] = this.noOfPatientsRate; @@ -301,6 +311,196 @@ class PatientAppointmentHistoryResponseModel { } } +class ListHISGetContactLensPrescription { + String? setupId; + int? projectId; + int? patientType; + int? patientId; + int? encounterType; + int? encounterNo; + int? oDOS; + dynamic brand; + dynamic baseCurve; + dynamic power; + dynamic diameter; + dynamic oZ; + dynamic cT; + dynamic blend; + String? remarks; + int? status; + bool? isActive; + String? createdOn; + + ListHISGetContactLensPrescription( + {this.setupId, + this.projectId, + this.patientType, + this.patientId, + this.encounterType, + this.encounterNo, + this.oDOS, + this.brand, + this.baseCurve, + this.power, + this.diameter, + this.oZ, + this.cT, + this.blend, + this.remarks, + this.status, + this.isActive, + this.createdOn}); + + ListHISGetContactLensPrescription.fromJson(Map json) { + setupId = json['SetupId']; + projectId = json['ProjectId']; + patientType = json['PatientType']; + patientId = json['PatientId']; + encounterType = json['EncounterType']; + encounterNo = json['EncounterNo']; + oDOS = json['OD_OS']; + brand = json['Brand']; + baseCurve = json['BaseCurve']; + power = json['Power']; + diameter = json['Diameter']; + oZ = json['OZ']; + cT = json['CT']; + blend = json['Blend']; + remarks = json['Remarks']; + status = json['Status']; + isActive = json['IsActive']; + createdOn = json['CreatedOn']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupId'] = this.setupId; + data['ProjectId'] = this.projectId; + data['PatientType'] = this.patientType; + data['PatientId'] = this.patientId; + data['EncounterType'] = this.encounterType; + data['EncounterNo'] = this.encounterNo; + data['OD_OS'] = this.oDOS; + data['Brand'] = this.brand; + data['BaseCurve'] = this.baseCurve; + data['Power'] = this.power; + data['Diameter'] = this.diameter; + data['OZ'] = this.oZ; + data['CT'] = this.cT; + data['Blend'] = this.blend; + data['Remarks'] = this.remarks; + data['Status'] = this.status; + data['IsActive'] = this.isActive; + data['CreatedOn'] = this.createdOn; + return data; + } +} + +class ListHISGetGlassPrescription { + dynamic projectID; + String? setupID; + dynamic patientId; + dynamic encounterType; + dynamic encounterNo; + String? visionType; + double? rightEyeSpherical; + dynamic rightEyeCylinder; + dynamic rightEyeAxis; + dynamic rightEyePrism; + dynamic rightEyeVA; + String? rightEyeRemarks; + dynamic leftEyeSpherical; + dynamic leftEyeCylinder; + dynamic leftEyeAxis; + dynamic leftEyePrism; + dynamic leftEyeVA; + String? leftEyeRemarks; + dynamic pD; + dynamic bVD; + dynamic status; + bool? isActive; + String? createdOn; + + ListHISGetGlassPrescription( + {this.projectID, + this.setupID, + this.patientId, + this.encounterType, + this.encounterNo, + this.visionType, + this.rightEyeSpherical, + this.rightEyeCylinder, + this.rightEyeAxis, + this.rightEyePrism, + this.rightEyeVA, + this.rightEyeRemarks, + this.leftEyeSpherical, + this.leftEyeCylinder, + this.leftEyeAxis, + this.leftEyePrism, + this.leftEyeVA, + this.leftEyeRemarks, + this.pD, + this.bVD, + this.status, + this.isActive, + this.createdOn}); + + ListHISGetGlassPrescription.fromJson(Map json) { + projectID = json['ProjectID']; + setupID = json['SetupID']; + patientId = json['PatientId']; + encounterType = json['EncounterType']; + encounterNo = json['EncounterNo']; + visionType = json['VisionType']; + rightEyeSpherical = json['RightEyeSpherical']; + rightEyeCylinder = json['RightEyeCylinder']; + rightEyeAxis = json['RightEyeAxis']; + rightEyePrism = json['RightEyePrism']; + rightEyeVA = json['RightEyeVA']; + rightEyeRemarks = json['RightEyeRemarks']; + leftEyeSpherical = json['LeftEyeSpherical']; + leftEyeCylinder = json['LeftEyeCylinder']; + leftEyeAxis = json['LeftEyeAxis']; + leftEyePrism = json['LeftEyePrism']; + leftEyeVA = json['LeftEyeVA']; + leftEyeRemarks = json['LeftEyeRemarks']; + pD = json['PD']; + bVD = json['BVD']; + status = json['Status']; + isActive = json['IsActive']; + createdOn = json['CreatedOn']; + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['SetupID'] = this.setupID; + data['PatientId'] = this.patientId; + data['EncounterType'] = this.encounterType; + data['EncounterNo'] = this.encounterNo; + data['VisionType'] = this.visionType; + data['RightEyeSpherical'] = this.rightEyeSpherical; + data['RightEyeCylinder'] = this.rightEyeCylinder; + data['RightEyeAxis'] = this.rightEyeAxis; + data['RightEyePrism'] = this.rightEyePrism; + data['RightEyeVA'] = this.rightEyeVA; + data['RightEyeRemarks'] = this.rightEyeRemarks; + data['LeftEyeSpherical'] = this.leftEyeSpherical; + data['LeftEyeCylinder'] = this.leftEyeCylinder; + data['LeftEyeAxis'] = this.leftEyeAxis; + data['LeftEyePrism'] = this.leftEyePrism; + data['LeftEyeVA'] = this.leftEyeVA; + data['LeftEyeRemarks'] = this.leftEyeRemarks; + data['PD'] = this.pD; + data['BVD'] = this.bVD; + data['Status'] = this.status; + data['IsActive'] = this.isActive; + data['CreatedOn'] = this.createdOn; + return data; + } +} + class PatientAppointmentList { String? filterName = ""; List? patientDoctorAppointmentList = []; diff --git a/lib/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart b/lib/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart index 2021fff..1dc1cbe 100644 --- a/lib/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart +++ b/lib/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart @@ -141,7 +141,7 @@ class PatientAppointmentShareResponseModel { doctorID = json['DoctorID']; doctorImageURL = json['DoctorImageURL']; doctorNameObj = json['DoctorNameObj']; - doctorSpeciality = json['DoctorSpeciality'].cast(); + // doctorSpeciality = json['DoctorSpeciality'].cast(); errCode = json['ErrCode']; groupID = json['GroupID']; iSAllowOnlineCheckedIN = json['ISAllowOnlineCheckedIN']; diff --git a/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart b/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart new file mode 100644 index 0000000..c36b9ec --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart @@ -0,0 +1,40 @@ +class PatientQueueDetails { + int? patientID; + String? patientName; + String? queueNo; + int? callType; + String? roomNo; + String? calledOn; + bool? servingNow; + + PatientQueueDetails( + {this.patientID, + this.patientName, + this.queueNo, + this.callType, + this.roomNo, + this.calledOn, + this.servingNow}); + + PatientQueueDetails.fromJson(Map json) { + patientID = json['patientID']; + patientName = json['patientName']; + queueNo = json['queueNo']; + callType = json['callType']; + roomNo = json['roomNo']; + calledOn = json['calledOn']; + servingNow = json['servingNow']; + } + + Map toJson() { + final Map data = new Map(); + data['patientID'] = this.patientID; + data['patientName'] = this.patientName; + data['queueNo'] = this.queueNo; + data['callType'] = this.callType; + data['roomNo'] = this.roomNo; + data['calledOn'] = this.calledOn; + data['servingNow'] = this.servingNow; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart new file mode 100644 index 0000000..877b7bc --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart @@ -0,0 +1,160 @@ +class RateAppointmentRespModel { + String? setupID; + int? projectID; + int? appointmentNo; + String? appointmentDate; + String? appointmentDateN; + int? appointmentType; + String? bookDate; + int? patientType; + int? patientID; + int? clinicID; + int? doctorID; + String? endDate; + String? startTime; + String? endTime; + int? status; + int? visitType; + int? visitFor; + int? patientStatusType; + int? companyID; + int? bookedBy; + String? bookedOn; + int? confirmedBy; + String? confirmedOn; + int? arrivalChangedBy; + String? arrivedOn; + int? editedBy; + String? editedOn; + dynamic doctorName; + String? doctorNameN; + String? statusDesc; + String? statusDescN; + bool? vitalStatus; + dynamic vitalSignAppointmentNo; + int? episodeID; + String? doctorTitle; + bool? isAppoitmentLiveCare; + + RateAppointmentRespModel( + {this.setupID, + this.projectID, + this.appointmentNo, + this.appointmentDate, + this.appointmentDateN, + this.appointmentType, + this.bookDate, + this.patientType, + this.patientID, + this.clinicID, + this.doctorID, + this.endDate, + this.startTime, + this.endTime, + this.status, + this.visitType, + this.visitFor, + this.patientStatusType, + this.companyID, + this.bookedBy, + this.bookedOn, + this.confirmedBy, + this.confirmedOn, + this.arrivalChangedBy, + this.arrivedOn, + this.editedBy, + this.editedOn, + this.doctorName, + this.doctorNameN, + this.statusDesc, + this.statusDescN, + this.vitalStatus, + this.vitalSignAppointmentNo, + this.episodeID, + this.doctorTitle, + this.isAppoitmentLiveCare}); + + RateAppointmentRespModel.fromJson(Map json) { + try { + setupID = json['SetupID']; + projectID = json['ProjectID']; + appointmentNo = json['AppointmentNo']; + appointmentDate = json['AppointmentDate']; + appointmentDateN = json['AppointmentDateN']; + appointmentType = json['AppointmentType']; + bookDate = json['BookDate']; + patientType = json['PatientType']; + patientID = json['PatientID']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + endDate = json['EndDate']; + startTime = json['StartTime']; + endTime = json['EndTime']; + status = json['Status']; + visitType = json['VisitType']; + visitFor = json['VisitFor']; + patientStatusType = json['PatientStatusType']; + companyID = json['CompanyID']; + bookedBy = json['BookedBy']; + bookedOn = json['BookedOn']; + confirmedBy = json['ConfirmedBy']; + confirmedOn = json['ConfirmedOn']; + arrivalChangedBy = json['ArrivalChangedBy']; + arrivedOn = json['ArrivedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + statusDesc = json['StatusDesc']; + statusDescN = json['StatusDescN']; + vitalStatus = json['VitalStatus']; + vitalSignAppointmentNo = json['VitalSignAppointmentNo']; + episodeID = json['EpisodeID']; + doctorTitle = json['DoctorTitle']; + isAppoitmentLiveCare = json['IsAppoitmentLiveCare']; + } catch (e) { + print(e); + } + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['AppointmentNo'] = this.appointmentNo; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentDateN'] = this.appointmentDateN; + data['AppointmentType'] = this.appointmentType; + data['BookDate'] = this.bookDate; + data['PatientType'] = this.patientType; + data['PatientID'] = this.patientID; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['EndDate'] = this.endDate; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['Status'] = this.status; + data['VisitType'] = this.visitType; + data['VisitFor'] = this.visitFor; + data['PatientStatusType'] = this.patientStatusType; + data['CompanyID'] = this.companyID; + data['BookedBy'] = this.bookedBy; + data['BookedOn'] = this.bookedOn; + data['ConfirmedBy'] = this.confirmedBy; + data['ConfirmedOn'] = this.confirmedOn; + data['ArrivalChangedBy'] = this.arrivalChangedBy; + data['ArrivedOn'] = this.arrivedOn; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['StatusDesc'] = this.statusDesc; + data['StatusDescN'] = this.statusDescN; + data['VitalStatus'] = this.vitalStatus; + data['VitalSignAppointmentNo'] = this.vitalSignAppointmentNo; + data['EpisodeID'] = this.episodeID; + data['DoctorTitle'] = this.doctorTitle; + data['IsAppoitmentLiveCare'] = this.isAppoitmentLiveCare; + return data; + } +} diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 87451c2..ce9e877 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -6,15 +6,21 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'models/req_model/appointment_rate_req_model.dart'; +import 'models/resp_models/appointment_details_resp_model.dart'; + abstract class MyAppointmentsRepo { - Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments}); + Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); Future>> getPatientShareAppointment( {required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment}); @@ -49,6 +55,24 @@ abstract class MyAppointmentsRepo { Future>> getTamaraInstallmentsDetails(); Future>> getActiveAppointmentsCount(); + + Future>>> getAskDoctorRequestTypes(); + + Future>> isDoctorAvailable({required int projectID, required int clinicID, required int doctorID}); + + Future>> sendAskDocCallRequest( + {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String requestType, required String remarks, required String userMobileNumber}); + + Future>>> getLastRatingAppointment(); + + Future>> getAppointmentDetails(int appointmentID, int projectID); + + + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note); + + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName); + + Future>>> getPatientAppointmentQueueDetails({required int appointmentNo, required int patientID}); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -58,13 +82,15 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { MyAppointmentsRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments}) async { + Future>>> getPatientAppointments( + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, "IsComingFromCOC": false, "isForUpcomming": false, "IsForMedicalReport": false, "IsForArrived": isArrivedAppointments, + "IsIrisPrescription": isForEyeMeasurement }; try { @@ -166,7 +192,9 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { "AppointmentNo": appointmentNo, "PaymentMethodName": paymentMethodName, "PaymentAmount": payedAmount == 0 ? "0" : payedAmount.toString(), - "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime.now().millisecondsSinceEpoch})/", + "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime + .now() + .millisecondsSinceEpoch})/", "PaymentReferenceNumber": payedAmount == 0 ? "" : paymentReference, "ProjectID": projectID, "PatientID": patientID, @@ -618,4 +646,315 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } -} + + @override + Future> isDoctorAvailable({required int projectID, required int clinicID, required int doctorID}) async { + Map mapDevice = {"isDentalAllowedBackend": false, "DoctorID": doctorID, "ProjectID": projectID, "ClinicID": clinicID}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + IS_DOCTOR_AVAILABLE_BY_CALENDAR_SCHEDULE, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final isDoctorAvailable = response['IsDoctorAvailable']; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: isDoctorAvailable, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getAskDoctorRequestTypes() async { + Map mapDevice = {}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(GET_CALL_REQUEST_TYPE, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListReqTypes']; + + final askDoctorRequestTypesList = list.map((item) => AskDocRequestType.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: askDoctorRequestTypesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> sendAskDocCallRequest( + {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String requestType, required String remarks, required String userMobileNumber}) async { + Map body = {}; + + body['ProjectID'] = patientAppointmentHistoryResponseModel.projectID; + body['SetupID'] = patientAppointmentHistoryResponseModel.setupID; + body['DoctorID'] = patientAppointmentHistoryResponseModel.doctorID; + body['PatientMobileNumber'] = userMobileNumber; + body['IsMessageSent'] = false; + body['RequestDate'] = DateUtil.convertDateToString(DateTime.now()); + body['RequestTime'] = DateUtil.convertDateToString(DateTime.now()); + body['Remarks'] = remarks; + body['Status'] = 2; // 4 for testing only.."cancelled status insert" else should be changed to 1 in live version + body['CreatedBy'] = 102; + body['CreatedOn'] = DateUtil.convertDateToString(DateTime.now()); + body['EditedBy'] = 102; + body['EditedOn'] = DateUtil.convertDateToString(DateTime.now()); + body['isDentalAllowedBackend'] = false; + body['AppointmentNo'] = patientAppointmentHistoryResponseModel.appointmentNo; + body['ClinicID'] = patientAppointmentHistoryResponseModel.clinicID; + body['QuestionType'] = num.parse(requestType); + body['RequestType'] = num.parse(requestType); + body['RequestTypeID'] = num.parse(requestType); + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(INSERT_CALL_INFO, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: body); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getPatientAppointmentQueueDetails({required int appointmentNo, required int patientID}) async { + Map mapDevice = {"appointmentNo": appointmentNo, "patientID": patientID, "apiKey": "EE17D21C7943485D9780223CCE55DCE5"}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(ApiConsts.QLINE_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['data']; + final queueList = list.map((item) => PatientQueueDetails.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: queueList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice, isExternal: true, isAllowAny: true); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getLastRatingAppointment() async { + Map mapDevice = {}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(IS_LAST_APPOITMENT_RATED, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['IsLastAppoitmentRatedList']; + + final lstRatingAppointmentList = list.map((item) => RateAppointmentRespModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: lstRatingAppointmentList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getAppointmentDetails(int appointmentID, int projectID) async { + Map mapDevice = { + "AppointmentNumber": appointmentID, + "ProjectID": projectID, + }; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(GET_APPOINTMENT_DETAILS_BY_NO, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentDetails']; + + final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentDetails, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note) async { + + AppointmentRate appointmentRate = AppointmentRate(); + appointmentRate.rate = rate; + appointmentRate.appointmentNo = appointmentNo; + appointmentRate.projectID = projectID; + appointmentRate.doctorID = doctorID; + appointmentRate.clinicID = clinicID; + appointmentRate.note = note; + appointmentRate.createdBy = 2; + appointmentRate.editedBy = 2; + + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_APPOINTMENT_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: appointmentRate.toJson()); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName) async { + Map request; + + request = { + "DoctorID": doctorID, + "Rate": rate, + "ClinicID": clinicID, + "ProjectID": projectID, + "AppointmentNo": appointmentNo, + "Note": note, + // "MobileNumber": authenticatedUserObject.user!.mobileNumber, + "AppointmentDate": appoDate, + "DoctorName": docName, + "ProjectName": projectName, + "COCTypeName": 1, + // "PatientName": authenticatedUserObject.user!.firstName! + " " + authenticatedUserObject.user!.lastName!, + // "PatientOutSA": authenticatedUserObject.user!.outSA, + // "PatientTypeID": authenticatedUserObject.user!.patientType, + "ClinicName": clinicName, + // "PatientIdentificationID": authenticatedUserObject.user!.patientIdentificationNo + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_DOCTOR_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + // final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: request); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + +} \ No newline at end of file diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index e8b2650..9bd48ec 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -13,6 +16,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { int selectedTabIndex = 0; int previouslySelectedTab = -1; + int eyeMeasurementsTabSelectedIndex = 0; + MyAppointmentsRepo myAppointmentsRepo; ErrorHandlerService errorHandlerService; AppState appState; @@ -26,12 +31,23 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isAppointmentDataToBeLoaded = true; + bool isEyeMeasurementsAppointmentsLoading = false; + List availableFilters = []; List? selectedFilter = []; bool isDateFilterSelected = false; DateTime? start = null; DateTime? end = null; + bool isAppointmentQueueDetailsLoading = false; + bool isPatientHasQueueAppointment = false; + int currentQueueStatus = 0; + List patientQueueDetailsList = []; + late PatientQueueDetails currentPatientQueueDetails; + + + + List patientAppointmentsHistoryList = []; List filteredAppointmentList = []; @@ -42,6 +58,16 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientMyDoctorsList = []; + List patientEyeMeasurementsAppointmentsHistoryList = []; + + // Grouping by Clinic and Hospital + List patientAppointmentsByClinic = []; + List patientAppointmentsByHospital = []; + List patientAppointmentsViewList = []; + bool isAppointmentsSortByClinic = true; + + List askDoctorRequestTypeList = []; + PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel; GetTamaraInstallmentsDetailsResponseModel? getTamaraInstallmentsDetailsResponseModel; @@ -57,18 +83,70 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + void onEyeMeasurementsTabChanged(int index) { + eyeMeasurementsTabSelectedIndex = index; + notifyListeners(); + } + + setIsAppointmentsSortByClinic(bool value) { + isAppointmentsSortByClinic = value; + _groupAppointmentsByClinicAndHospital(); + notifyListeners(); + } + + void _groupAppointmentsByClinicAndHospital() { + patientAppointmentsByClinic.clear(); + patientAppointmentsByHospital.clear(); + + for (var element in filteredAppointmentList) { + // Group by clinic + List appointmentsByClinic = patientAppointmentsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList(); + + if (appointmentsByClinic.isNotEmpty) { + patientAppointmentsByClinic[patientAppointmentsByClinic.indexOf(appointmentsByClinic[0])].patientDoctorAppointmentList!.add(element); + } else { + patientAppointmentsByClinic.add(PatientAppointmentList(filterName: element.clinicName, patientDoctorAppointment: element)); + } + + // Group by hospital + List appointmentsByHospital = patientAppointmentsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (appointmentsByHospital.isNotEmpty) { + patientAppointmentsByHospital[patientAppointmentsByHospital.indexOf(appointmentsByHospital[0])].patientDoctorAppointmentList!.add(element); + } else { + patientAppointmentsByHospital.add(PatientAppointmentList(filterName: element.projectName, patientDoctorAppointment: element)); + } + } + + if (isAppointmentsSortByClinic) { + patientAppointmentsViewList = patientAppointmentsByClinic; + } else { + patientAppointmentsViewList = patientAppointmentsByHospital; + } + } + initAppointmentsViewModel() { if (isAppointmentDataToBeLoaded) { + // Default view is grouped by clinic on first open. + isAppointmentsSortByClinic = true; patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); patientTimelineAppointmentsList.clear(); + patientEyeMeasurementsAppointmentsHistoryList.clear(); isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; patientMyDoctorsList.clear(); } isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; + isEyeMeasurementsAppointmentsLoading = true; + + notifyListeners(); + } + + setCurrentQueueStatus(int currentQueueStatus) { + this.currentQueueStatus = currentQueueStatus; notifyListeners(); } @@ -105,6 +183,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsEyeMeasurementsAppointmentsLoading(bool val) { + isEyeMeasurementsAppointmentsLoading = val; + notifyListeners(); + } + setAppointmentReminder(bool value, PatientAppointmentHistoryResponseModel item) { int index = patientAppointmentsHistoryList.indexOf(item); if (index != -1) { @@ -113,6 +196,29 @@ class MyAppointmentsViewModel extends ChangeNotifier { } } + Future getPatientEyeMeasurementAppointments({Function(dynamic)? onSuccess, Function(String)? onError}) async { + patientEyeMeasurementsAppointmentsHistoryList.clear(); + notifyListeners(); + + final result = await myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForEyeMeasurement: true); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientEyeMeasurementsAppointmentsHistoryList = apiResponse.data!; + isEyeMeasurementsAppointmentsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {Function(dynamic)? onSuccess, Function(String)? onError}) async { if (!isAppointmentDataToBeLoaded) return; @@ -165,10 +271,21 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientAppointmentsHistoryList); + // Build grouped list immediately so the UI has data for the default (By Clinic) view. + _groupAppointmentsByClinicAndHospital(); + + if (patientArrivedAppointmentsHistoryList.isNotEmpty) { + if (Utils.isDateToday(DateUtil.convertStringToDate(patientArrivedAppointmentsHistoryList.first.appointmentDate))) { + // getPatientAppointmentQueueDetails(appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID); + getPatientAppointmentQueueDetails(); + } + } + print('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); + notifyListeners(); } void getFiltersForSelectedAppointmentList(List filteredAppointmentList) { @@ -445,6 +562,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { break; } getFiltersForSelectedAppointmentList(filteredAppointmentList); + _groupAppointmentsByClinicAndHospital(); notifyListeners(); } @@ -502,6 +620,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } catch (e) {} } } + _groupAppointmentsByClinicAndHospital(); notifyListeners(); } @@ -568,4 +687,117 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + + Future isDoctorAvailable({required int projectID, required int doctorId, required int clinicId, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.isDoctorAvailable(projectID: projectID, doctorID: doctorId, clinicID: clinicId); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future getAskDoctorRequestTypes({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getAskDoctorRequestTypes(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + askDoctorRequestTypeList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future getPatientAppointmentQueueDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isAppointmentQueueDetailsLoading = true; + notifyListeners(); + final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails( + appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID); + + isAppointmentQueueDetailsLoading = false; + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data != null && apiResponse.data!.isNotEmpty) { + isPatientHasQueueAppointment = true; + patientQueueDetailsList = apiResponse.data!; + for (var element in patientQueueDetailsList) { + if (element.patientID == patientArrivedAppointmentsHistoryList.first.patientID) { + currentPatientQueueDetails = element; + currentQueueStatus = element.callType!; + // currentQueueStatus = 2; + break; + } + } + // patientQueueDetailsList.first.callType = 1; + + patientQueueDetailsList.removeWhere((element) => element.patientID == patientArrivedAppointmentsHistoryList.first.patientID); + } else { + isPatientHasQueueAppointment = false; + } + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future sendAskDocCallRequest({ + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + required String requestType, + required String remarks, + required String userMobileNumber, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await myAppointmentsRepo.sendAskDocCallRequest( + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, requestType: requestType, remarks: remarks, userMobileNumber: userMobileNumber); + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) onError(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + } diff --git a/lib/features/my_invoices/models/get_invoice_details_response_model.dart b/lib/features/my_invoices/models/get_invoice_details_response_model.dart new file mode 100644 index 0000000..ef88623 --- /dev/null +++ b/lib/features/my_invoices/models/get_invoice_details_response_model.dart @@ -0,0 +1,489 @@ +class GetInvoiceDetailsResponseModel { + int? projectID; + int? doctorID; + num? grandTotal; + num? quantity; + num? total; + num? discount; + num? subTotal; + int? invoiceNo; + String? createdOn; + String? procedureID; + String? procedureName; + String? procedureNameN; + num? procedurePrice; + num? patientShare; + num? companyShare; + num? totalPatientShare; + num? totalCompanyShare; + num? totalShare; + num? discountAmount; + num? vATPercentage; + num? patientVATAmount; + num? companyVATAmount; + num? totalVATAmount; + num? price; + int? patientID; + String? patientIdentificationNo; + String? patientName; + String? patientNameN; + String? nationalityID; + String? doctorName; + String? doctorNameN; + int? clinicID; + String? clinicDescription; + String? clinicDescriptionN; + String? appointmentDate; + int? appointmentNo; + String? insuranceID; + int? companyID; + String? companyName; + String? companyNameN; + String? companyAddress; + String? companyAddressN; + String? companyGroupAddress; + String? groupName; + String? groupNameN; + String? patientAddress; + String? vATNo; + String? paymentDate; + String? projectName; + num? totalDiscount; + num? totalPatientShareWithQuantity; + String? legalName; + String? legalNameN; + num? advanceAdjustment; + String? patientCityName; + String? patientCityNameN; + String? doctorImageURL; + List? listConsultation; + + GetInvoiceDetailsResponseModel( + {this.projectID, + this.doctorID, + this.grandTotal, + this.quantity, + this.total, + this.discount, + this.subTotal, + this.invoiceNo, + this.createdOn, + this.procedureID, + this.procedureName, + this.procedureNameN, + this.procedurePrice, + this.patientShare, + this.companyShare, + this.totalPatientShare, + this.totalCompanyShare, + this.totalShare, + this.discountAmount, + this.vATPercentage, + this.patientVATAmount, + this.companyVATAmount, + this.totalVATAmount, + this.price, + this.patientID, + this.patientIdentificationNo, + this.patientName, + this.patientNameN, + this.nationalityID, + this.doctorName, + this.doctorNameN, + this.clinicID, + this.clinicDescription, + this.clinicDescriptionN, + this.appointmentDate, + this.appointmentNo, + this.insuranceID, + this.companyID, + this.companyName, + this.companyNameN, + this.companyAddress, + this.companyAddressN, + this.companyGroupAddress, + this.groupName, + this.groupNameN, + this.patientAddress, + this.vATNo, + this.paymentDate, + this.projectName, + this.totalDiscount, + this.totalPatientShareWithQuantity, + this.legalName, + this.legalNameN, + this.advanceAdjustment, + this.patientCityName, + this.patientCityNameN, + this.doctorImageURL, + this.listConsultation}); + + GetInvoiceDetailsResponseModel.fromJson(Map json) { + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + grandTotal = json['GrandTotal']; + quantity = json['Quantity']; + total = json['Total']; + discount = json['Discount']; + subTotal = json['SubTotal']; + invoiceNo = json['InvoiceNo']; + createdOn = json['CreatedOn']; + procedureID = json['ProcedureID']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + procedurePrice = json['ProcedurePrice']; + patientShare = json['PatientShare']; + companyShare = json['CompanyShare']; + totalPatientShare = json['TotalPatientShare']; + totalCompanyShare = json['TotalCompanyShare']; + totalShare = json['TotalShare']; + discountAmount = json['DiscountAmount']; + vATPercentage = json['VATPercentage']; + patientVATAmount = json['PatientVATAmount']; + companyVATAmount = json['CompanyVATAmount']; + totalVATAmount = json['TotalVATAmount']; + price = json['Price']; + patientID = json['PatientID']; + patientIdentificationNo = json['PatientIdentificationNo']; + patientName = json['PatientName']; + patientNameN = json['PatientNameN']; + nationalityID = json['NationalityID']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicID = json['ClinicID']; + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + appointmentDate = json['AppointmentDate']; + appointmentNo = json['AppointmentNo']; + insuranceID = json['InsuranceID']; + companyID = json['CompanyID']; + companyName = json['CompanyName']; + companyNameN = json['CompanyNameN']; + companyAddress = json['CompanyAddress']; + companyAddressN = json['CompanyAddressN']; + companyGroupAddress = json['CompanyGroupAddress']; + groupName = json['GroupName']; + groupNameN = json['GroupNameN']; + patientAddress = json['PatientAddress']; + vATNo = json['VATNo']; + paymentDate = json['PaymentDate']; + projectName = json['ProjectName']; + totalDiscount = json['TotalDiscount']; + totalPatientShareWithQuantity = json['TotalPatientShareWithQuantity']; + legalName = json['LegalName']; + legalNameN = json['LegalNameN']; + advanceAdjustment = json['AdvanceAdjustment']; + patientCityName = json['PatientCityName']; + patientCityNameN = json['PatientCityNameN']; + doctorImageURL = json['DoctorImageURL']; + if (json['listConsultation'] != null) { + listConsultation = []; + json['listConsultation'].forEach((v) { + listConsultation!.add(new ListConsultation.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['GrandTotal'] = this.grandTotal; + data['Quantity'] = this.quantity; + data['Total'] = this.total; + data['Discount'] = this.discount; + data['SubTotal'] = this.subTotal; + data['InvoiceNo'] = this.invoiceNo; + data['CreatedOn'] = this.createdOn; + data['ProcedureID'] = this.procedureID; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + data['ProcedurePrice'] = this.procedurePrice; + data['PatientShare'] = this.patientShare; + data['CompanyShare'] = this.companyShare; + data['TotalPatientShare'] = this.totalPatientShare; + data['TotalCompanyShare'] = this.totalCompanyShare; + data['TotalShare'] = this.totalShare; + data['DiscountAmount'] = this.discountAmount; + data['VATPercentage'] = this.vATPercentage; + data['PatientVATAmount'] = this.patientVATAmount; + data['CompanyVATAmount'] = this.companyVATAmount; + data['TotalVATAmount'] = this.totalVATAmount; + data['Price'] = this.price; + data['PatientID'] = this.patientID; + data['PatientIdentificationNo'] = this.patientIdentificationNo; + data['PatientName'] = this.patientName; + data['PatientNameN'] = this.patientNameN; + data['NationalityID'] = this.nationalityID; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicID'] = this.clinicID; + data['ClinicDescription'] = this.clinicDescription; + data['ClinicDescriptionN'] = this.clinicDescriptionN; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentNo'] = this.appointmentNo; + data['InsuranceID'] = this.insuranceID; + data['CompanyID'] = this.companyID; + data['CompanyName'] = this.companyName; + data['CompanyNameN'] = this.companyNameN; + data['CompanyAddress'] = this.companyAddress; + data['CompanyAddressN'] = this.companyAddressN; + data['CompanyGroupAddress'] = this.companyGroupAddress; + data['GroupName'] = this.groupName; + data['GroupNameN'] = this.groupNameN; + data['PatientAddress'] = this.patientAddress; + data['VATNo'] = this.vATNo; + data['PaymentDate'] = this.paymentDate; + data['ProjectName'] = this.projectName; + data['TotalDiscount'] = this.totalDiscount; + data['TotalPatientShareWithQuantity'] = this.totalPatientShareWithQuantity; + data['LegalName'] = this.legalName; + data['LegalNameN'] = this.legalNameN; + data['AdvanceAdjustment'] = this.advanceAdjustment; + data['PatientCityName'] = this.patientCityName; + data['PatientCityNameN'] = this.patientCityNameN; + data['DoctorImageURL'] = this.doctorImageURL; + if (this.listConsultation != null) { + data['listConsultation'] = + this.listConsultation!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class ListConsultation { + int? projectID; + int? doctorID; + num? grandTotal; + int? quantity; + num? total; + num? discount; + num? subTotal; + int? invoiceNo; + String? createdOn; + String? procedureID; + String? procedureName; + String? procedureNameN; + num? procedurePrice; + num? patientShare; + num? companyShare; + num? totalPatientShare; + num? totalCompanyShare; + num? totalShare; + num? discountAmount; + num? vATPercentage; + num? patientVATAmount; + num? companyVATAmount; + num? totalVATAmount; + num? price; + int? patientID; + int? patientIdentificationNo; + String? patientName; + String? patientNameN; + String? nationalityID; + String? doctorName; + String? doctorNameN; + int? clinicID; + String? clinicDescription; + String? clinicDescriptionN; + String? appointmentDate; + dynamic appointmentNo; + dynamic insuranceID; + dynamic companyID; + String? companyName; + String? companyNameN; + String? companyAddress; + String? companyAddressN; + String? companyGroupAddress; + String? groupName; + String? groupNameN; + String? patientAddress; + String? vATNo; + String? paymentDate; + String? projectName; + num? totalDiscount; + num? totalPatientShareWithQuantity; + String? legalName; + String? legalNameN; + num? advanceAdjustment; + String? patientCityName; + String? patientCityNameN; + + ListConsultation( + {this.projectID, + this.doctorID, + this.grandTotal, + this.quantity, + this.total, + this.discount, + this.subTotal, + this.invoiceNo, + this.createdOn, + this.procedureID, + this.procedureName, + this.procedureNameN, + this.procedurePrice, + this.patientShare, + this.companyShare, + this.totalPatientShare, + this.totalCompanyShare, + this.totalShare, + this.discountAmount, + this.vATPercentage, + this.patientVATAmount, + this.companyVATAmount, + this.totalVATAmount, + this.price, + this.patientID, + this.patientIdentificationNo, + this.patientName, + this.patientNameN, + this.nationalityID, + this.doctorName, + this.doctorNameN, + this.clinicID, + this.clinicDescription, + this.clinicDescriptionN, + this.appointmentDate, + this.appointmentNo, + this.insuranceID, + this.companyID, + this.companyName, + this.companyNameN, + this.companyAddress, + this.companyAddressN, + this.companyGroupAddress, + this.groupName, + this.groupNameN, + this.patientAddress, + this.vATNo, + this.paymentDate, + this.projectName, + this.totalDiscount, + this.totalPatientShareWithQuantity, + this.legalName, + this.legalNameN, + this.advanceAdjustment, + this.patientCityName, + this.patientCityNameN}); + + ListConsultation.fromJson(Map json) { + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + grandTotal = json['GrandTotal']; + quantity = json['Quantity']; + total = json['Total']; + discount = json['Discount']; + subTotal = json['SubTotal']; + invoiceNo = json['InvoiceNo']; + createdOn = json['CreatedOn']; + procedureID = json['ProcedureID']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + procedurePrice = json['ProcedurePrice']; + patientShare = json['PatientShare']; + companyShare = json['CompanyShare']; + totalPatientShare = json['TotalPatientShare']; + totalCompanyShare = json['TotalCompanyShare']; + totalShare = json['TotalShare']; + discountAmount = json['DiscountAmount']; + vATPercentage = json['VATPercentage']; + patientVATAmount = json['PatientVATAmount']; + companyVATAmount = json['CompanyVATAmount']; + totalVATAmount = json['TotalVATAmount']; + price = json['Price']; + patientID = json['PatientID']; + patientIdentificationNo = json['PatientIdentificationNo']; + patientName = json['PatientName']; + patientNameN = json['PatientNameN']; + nationalityID = json['NationalityID']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicID = json['ClinicID']; + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + appointmentDate = json['AppointmentDate']; + appointmentNo = json['AppointmentNo']; + insuranceID = json['InsuranceID']; + companyID = json['CompanyID']; + companyName = json['CompanyName']; + companyNameN = json['CompanyNameN']; + companyAddress = json['CompanyAddress']; + companyAddressN = json['CompanyAddressN']; + companyGroupAddress = json['CompanyGroupAddress']; + groupName = json['GroupName']; + groupNameN = json['GroupNameN']; + patientAddress = json['PatientAddress']; + vATNo = json['VATNo']; + paymentDate = json['PaymentDate']; + projectName = json['ProjectName']; + totalDiscount = json['TotalDiscount']; + totalPatientShareWithQuantity = json['TotalPatientShareWithQuantity']; + legalName = json['LegalName']; + legalNameN = json['LegalNameN']; + advanceAdjustment = json['AdvanceAdjustment']; + patientCityName = json['PatientCityName']; + patientCityNameN = json['PatientCityNameN']; + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['GrandTotal'] = this.grandTotal; + data['Quantity'] = this.quantity; + data['Total'] = this.total; + data['Discount'] = this.discount; + data['SubTotal'] = this.subTotal; + data['InvoiceNo'] = this.invoiceNo; + data['CreatedOn'] = this.createdOn; + data['ProcedureID'] = this.procedureID; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + data['ProcedurePrice'] = this.procedurePrice; + data['PatientShare'] = this.patientShare; + data['CompanyShare'] = this.companyShare; + data['TotalPatientShare'] = this.totalPatientShare; + data['TotalCompanyShare'] = this.totalCompanyShare; + data['TotalShare'] = this.totalShare; + data['DiscountAmount'] = this.discountAmount; + data['VATPercentage'] = this.vATPercentage; + data['PatientVATAmount'] = this.patientVATAmount; + data['CompanyVATAmount'] = this.companyVATAmount; + data['TotalVATAmount'] = this.totalVATAmount; + data['Price'] = this.price; + data['PatientID'] = this.patientID; + data['PatientIdentificationNo'] = this.patientIdentificationNo; + data['PatientName'] = this.patientName; + data['PatientNameN'] = this.patientNameN; + data['NationalityID'] = this.nationalityID; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicID'] = this.clinicID; + data['ClinicDescription'] = this.clinicDescription; + data['ClinicDescriptionN'] = this.clinicDescriptionN; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentNo'] = this.appointmentNo; + data['InsuranceID'] = this.insuranceID; + data['CompanyID'] = this.companyID; + data['CompanyName'] = this.companyName; + data['CompanyNameN'] = this.companyNameN; + data['CompanyAddress'] = this.companyAddress; + data['CompanyAddressN'] = this.companyAddressN; + data['CompanyGroupAddress'] = this.companyGroupAddress; + data['GroupName'] = this.groupName; + data['GroupNameN'] = this.groupNameN; + data['PatientAddress'] = this.patientAddress; + data['VATNo'] = this.vATNo; + data['PaymentDate'] = this.paymentDate; + data['ProjectName'] = this.projectName; + data['TotalDiscount'] = this.totalDiscount; + data['TotalPatientShareWithQuantity'] = this.totalPatientShareWithQuantity; + data['LegalName'] = this.legalName; + data['LegalNameN'] = this.legalNameN; + data['AdvanceAdjustment'] = this.advanceAdjustment; + data['PatientCityName'] = this.patientCityName; + data['PatientCityNameN'] = this.patientCityNameN; + return data; + } +} diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart new file mode 100644 index 0000000..e8056d9 --- /dev/null +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -0,0 +1,88 @@ +class GetInvoicesListResponseModel { + String? setupId; + int? projectID; + int? patientID; + int? appointmentNo; + String? appointmentDate; + String? appointmentDateN; + int? clinicID; + int? doctorID; + int? invoiceNo; + int? status; + String? arrivedOn; + String? doctorName; + String? doctorNameN; + String? clinicName; + double? decimalDoctorRate; + String? doctorImageURL; + int? doctorRate; + int? patientNumber; + String? projectName; + + GetInvoicesListResponseModel( + {this.setupId, + this.projectID, + this.patientID, + this.appointmentNo, + this.appointmentDate, + this.appointmentDateN, + this.clinicID, + this.doctorID, + this.invoiceNo, + this.status, + this.arrivedOn, + this.doctorName, + this.doctorNameN, + this.clinicName, + this.decimalDoctorRate, + this.doctorImageURL, + this.doctorRate, + this.patientNumber, + this.projectName}); + + GetInvoicesListResponseModel.fromJson(Map json) { + setupId = json['SetupId']; + projectID = json['ProjectID']; + patientID = json['PatientID']; + appointmentNo = json['AppointmentNo']; + appointmentDate = json['AppointmentDate']; + appointmentDateN = json['AppointmentDateN']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + invoiceNo = json['InvoiceNo']; + status = json['Status']; + arrivedOn = json['ArrivedOn']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicName = json['ClinicName']; + decimalDoctorRate = json['DecimalDoctorRate']; + doctorImageURL = json['DoctorImageURL']; + doctorRate = json['DoctorRate']; + patientNumber = json['PatientNumber']; + projectName = json['ProjectName']; + } + + Map toJson() { + final Map data = {}; + data['SetupId'] = this.setupId; + data['ProjectID'] = this.projectID; + data['PatientID'] = this.patientID; + data['AppointmentNo'] = this.appointmentNo; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentDateN'] = this.appointmentDateN; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['InvoiceNo'] = this.invoiceNo; + data['Status'] = this.status; + data['ArrivedOn'] = this.arrivedOn; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicName'] = this.clinicName; + data['DecimalDoctorRate'] = this.decimalDoctorRate; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorRate'] = this.doctorRate; + data['PatientNumber'] = this.patientNumber; + data['ProjectName'] = this.projectName; + return data; + } +} diff --git a/lib/features/my_invoices/my_invoices_repo.dart b/lib/features/my_invoices/my_invoices_repo.dart new file mode 100644 index 0000000..68eee6e --- /dev/null +++ b/lib/features/my_invoices/my_invoices_repo.dart @@ -0,0 +1,141 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class MyInvoicesRepo { + Future>>> getAllInvoicesList(); + + Future>> getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID}); + + Future>> sendInvoiceEmail({required num appointmentNo, required int projectID}); +} + +class MyInvoicesRepoImp implements MyInvoicesRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MyInvoicesRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getAllInvoicesList() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_ALL_APPOINTMENTS_FOR_DENTAL_CLINIC, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_DentalAppointments']; + + final invoicesList = list.map((item) => GetInvoicesListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: invoicesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID}) async { + Map mapDevice = { + "AppointmentNo": appointmentNo, + "InvoiceNo": invoiceNo, + "IsRegistered": true, + "ProjectID": projectID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_DENTAL_APPOINTMENT_INVOICE, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_eInvoiceForDental']; + final invoicesList = GetInvoiceDetailsResponseModel.fromJson(list[0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: invoicesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> sendInvoiceEmail({required num appointmentNo, required int projectID}) async { + Map mapDevice = { + "AppointmentNo": appointmentNo, + "IsRegistered": true, + "ProjectID": projectID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + SEND_DENTAL_APPOINTMENT_INVOICE_EMAIL, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/my_invoices/my_invoices_view_model.dart b/lib/features/my_invoices/my_invoices_view_model.dart new file mode 100644 index 0000000..a02d741 --- /dev/null +++ b/lib/features/my_invoices/my_invoices_view_model.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; + +class MyInvoicesViewModel extends ChangeNotifier { + bool isInvoicesListLoading = false; + bool isInvoiceDetailsLoading = false; + + MyInvoicesRepo myInvoicesRepo; + ErrorHandlerService errorHandlerService; + NavigationService navServices; + + List allInvoicesList = []; + late GetInvoiceDetailsResponseModel invoiceDetailsResponseModel; + + MyInvoicesViewModel({required this.myInvoicesRepo, required this.errorHandlerService, required this.navServices}); + + setInvoicesListLoading() { + isInvoicesListLoading = true; + allInvoicesList.clear(); + notifyListeners(); + } + + setInvoiceDetailLoading() { + isInvoiceDetailsLoading = true; + notifyListeners(); + } + + Future getAllInvoicesList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.getAllInvoicesList(); + + result.fold( + (failure) async { + isInvoicesListLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + allInvoicesList = apiResponse.data!; + isInvoicesListLoading = false; + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.getInvoiceDetails(appointmentNo: appointmentNo, invoiceNo: invoiceNo, projectID: projectID); + + result.fold( + (failure) async { + isInvoiceDetailsLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + invoiceDetailsResponseModel = apiResponse.data!; + isInvoiceDetailsLoading = false; + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future sendInvoiceEmail({required num appointmentNo, required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.sendInvoiceEmail(appointmentNo: appointmentNo, projectID: projectID); + + result.fold( + (failure) async { + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } +} diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart index 74a323a..c00b01c 100644 --- a/lib/features/payfort/payfort_repo.dart +++ b/lib/features/payfort/payfort_repo.dart @@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart'; @@ -25,6 +26,8 @@ abstract class PayfortRepo { Future>> updateTamaraRequestStatus( {required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID}); + + Future>> getTamaraInstallmentsDetails(); } class PayfortRepoImp implements PayfortRepo { @@ -250,4 +253,42 @@ class PayfortRepoImp implements PayfortRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getTamaraInstallmentsDetails() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.get( + ApiConsts.GET_TAMARA_INSTALLMENTS_URL, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response; + + final tamaraInstallmentsList = GetTamaraInstallmentsDetailsResponseModel.fromJson(list.first); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: tamaraInstallmentsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + } diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index 89effcd..dda8f51 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -1,6 +1,9 @@ +import 'dart:developer'; + import 'package:amazon_payfort/amazon_payfort.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart'; @@ -21,6 +24,9 @@ class PayfortViewModel extends ChangeNotifier { late AmazonPayfort _payfort; final NetworkInfo _info = NetworkInfo(); + GetTamaraInstallmentsDetailsResponseModel? getTamaraInstallmentsDetailsResponseModel; + bool isTamaraDetailsLoading = false; + PayfortViewModel({required this.payfortRepo, required this.errorHandlerService}); setIsApplePayConfigurationLoading(bool value) { @@ -36,7 +42,8 @@ class PayfortViewModel extends ChangeNotifier { notifyListeners(); } - Future getPayfortConfigurations({int? serviceId, int? projectId, int integrationId = 2, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPayfortConfigurations( + {int? serviceId, int? projectId, int integrationId = 2, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await payfortRepo.getPayfortConfigurations(serviceId: serviceId, projectId: projectId, integrationId: integrationId); result.fold( @@ -56,7 +63,8 @@ class PayfortViewModel extends ChangeNotifier { ); } - Future applePayRequestInsert({required ApplePayInsertRequest applePayInsertRequest, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future applePayRequestInsert( + {required ApplePayInsertRequest applePayInsertRequest, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await payfortRepo.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest); result.fold( @@ -102,7 +110,7 @@ class PayfortViewModel extends ChangeNotifier { onError!(failure.message); }, (apiResponse) { - print(apiResponse.data); + log(apiResponse.data.toString()); if (onSuccess != null) { onSuccess(apiResponse); } @@ -112,15 +120,21 @@ class PayfortViewModel extends ChangeNotifier { } Future updateTamaraRequestStatus( - {required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID, Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await payfortRepo.updateTamaraRequestStatus(responseMessage: responseMessage, status: status, clientRequestID: clientRequestID, tamaraOrderID: tamaraOrderID); + {required String responseMessage, + required String status, + required String clientRequestID, + required String tamaraOrderID, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + final result = await payfortRepo.updateTamaraRequestStatus( + responseMessage: responseMessage, status: status, clientRequestID: clientRequestID, tamaraOrderID: tamaraOrderID); result.fold( (failure) async { onError!(failure.message); }, (apiResponse) { - print(apiResponse.data); + log(apiResponse.data.toString()); if (onSuccess != null) { onSuccess(apiResponse); } @@ -134,7 +148,7 @@ class PayfortViewModel extends ChangeNotifier { String? applePayShaType, String? applePayShaRequestPhrase, }) async { - var sdkTokenResponse; + SdkTokenResponse? sdkTokenResponse; try { String? deviceId = await _payfort.getDeviceId(); @@ -168,7 +182,7 @@ class PayfortViewModel extends ChangeNotifier { }, ); } catch (e) { - print("Error here: ${e.toString()}"); + log("Error here: ${e.toString()}"); } return sdkTokenResponse; } @@ -234,7 +248,8 @@ class PayfortViewModel extends ChangeNotifier { } } - Future markAppointmentAsTamaraPaid({required int projectID, required int appointmentNo, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future markAppointmentAsTamaraPaid( + {required int projectID, required int appointmentNo, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await payfortRepo.markAppointmentAsTamaraPaid(projectID: projectID, appointmentNo: appointmentNo); result.fold( @@ -242,11 +257,28 @@ class PayfortViewModel extends ChangeNotifier { onError!(failure.message); }, (apiResponse) { - print(apiResponse.data); + log(apiResponse.data); + if (onSuccess != null) { + onSuccess(apiResponse); + } + }, + ); + } + + Future getTamaraInstallmentsDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.getTamaraInstallmentsDetails(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + getTamaraInstallmentsDetailsResponseModel = apiResponse.data!; + isTamaraDetailsLoading = false; + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); } }, ); } + } diff --git a/lib/features/prescriptions/models/resp_models/prescription_delivery_response_model.dart b/lib/features/prescriptions/models/resp_models/prescription_delivery_response_model.dart new file mode 100644 index 0000000..c221e90 --- /dev/null +++ b/lib/features/prescriptions/models/resp_models/prescription_delivery_response_model.dart @@ -0,0 +1,318 @@ +class PrescriptionDeliveryResponseModel { + int? iD; + int? patientId; + int? patientOutSa; + bool? isOutPatient; + int? projectId; + int? nearestProjectId; + dynamic longitude; + dynamic latitude; + dynamic appointmentNo; + dynamic dischargeId; + int? statusId; + int? serviceId; + int? channel; + Orderpayment? orderpayment; + dynamic orderselectedservice; + dynamic wforder; + dynamic orderapprovalobj; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + String? statusText; + int? paymentStatus; + dynamic clientRequestid; + dynamic paymentStatusText; + String? projectName; + String? nearestProjectName; + dynamic paymentAmount; + WFOrder? wFOrder; + String? serviceText; + bool? isSentForApproval; + int? exaCartOrderId; + bool? isTimer; + int? timeSeconds; + int? totalPendingSeconds; + int? timeMinute; + int? timeHour; + int? timeTotalSeconds; + int? timeTotalMinute; + int? timeTotalHour; + dynamic approvalStatus; + bool? isActive; + int? clickButton; + List? procedures; + dynamic pickupLocation; + dynamic dropOffLocation; + dynamic clinicName; + dynamic doctorName; + dynamic branch; + dynamic time; + dynamic notes; + + PrescriptionDeliveryResponseModel( + {this.iD, + this.patientId, + this.patientOutSa, + this.isOutPatient, + this.projectId, + this.nearestProjectId, + this.longitude, + this.latitude, + this.appointmentNo, + this.dischargeId, + this.statusId, + this.serviceId, + this.channel, + this.orderpayment, + this.orderselectedservice, + this.wforder, + this.orderapprovalobj, + this.created, + this.createdBy, + this.modified, + this.modifiedBy, + this.isDeleted, + this.statusText, + this.paymentStatus, + this.clientRequestid, + this.paymentStatusText, + this.projectName, + this.nearestProjectName, + this.paymentAmount, + this.wFOrder, + this.serviceText, + this.isSentForApproval, + this.exaCartOrderId, + this.isTimer, + this.timeSeconds, + this.totalPendingSeconds, + this.timeMinute, + this.timeHour, + this.timeTotalSeconds, + this.timeTotalMinute, + this.timeTotalHour, + this.approvalStatus, + this.isActive, + this.clickButton, + this.procedures, + this.pickupLocation, + this.dropOffLocation, + this.clinicName, + this.doctorName, + this.branch, + this.time, + this.notes}); + + PrescriptionDeliveryResponseModel.fromJson(Map json) { + iD = json['ID']; + patientId = json['PatientId']; + patientOutSa = json['PatientOutSa']; + isOutPatient = json['IsOutPatient']; + projectId = json['ProjectId']; + nearestProjectId = json['NearestProjectId']; + longitude = json['Longitude']; + latitude = json['Latitude']; + appointmentNo = json['AppointmentNo']; + dischargeId = json['DischargeId']; + statusId = json['StatusId']; + serviceId = json['ServiceId']; + channel = json['Channel']; + orderpayment = json['orderpayment'] != null ? new Orderpayment.fromJson(json['orderpayment']) : null; + orderselectedservice = json['orderselectedservice']; + wforder = json['wforder']; + orderapprovalobj = json['orderapprovalobj']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + statusText = json['StatusText']; + paymentStatus = json['PaymentStatus']; + clientRequestid = json['ClientRequestid']; + paymentStatusText = json['PaymentStatusText']; + projectName = json['ProjectName']; + nearestProjectName = json['NearestProjectName']; + paymentAmount = json['PaymentAmount']; + wFOrder = json['WF_order'] != null ? new WFOrder.fromJson(json['WF_order']) : null; + serviceText = json['ServiceText']; + isSentForApproval = json['isSentForApproval']; + exaCartOrderId = json['ExaCart_OrderId']; + isTimer = json['isTimer']; + timeSeconds = json['TimeSeconds']; + totalPendingSeconds = json['TotalPendingSeconds']; + timeMinute = json['TimeMinute']; + timeHour = json['TimeHour']; + timeTotalSeconds = json['TimeTotalSeconds']; + timeTotalMinute = json['TimeTotalMinute']; + timeTotalHour = json['TimeTotalHour']; + approvalStatus = json['ApprovalStatus']; + isActive = json['isActive']; + clickButton = json['ClickButton']; + pickupLocation = json['PickupLocation']; + dropOffLocation = json['DropOffLocation']; + clinicName = json['clinicName']; + doctorName = json['DoctorName']; + branch = json['Branch']; + time = json['Time']; + notes = json['Notes']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['PatientId'] = this.patientId; + data['PatientOutSa'] = this.patientOutSa; + data['IsOutPatient'] = this.isOutPatient; + data['ProjectId'] = this.projectId; + data['NearestProjectId'] = this.nearestProjectId; + data['Longitude'] = this.longitude; + data['Latitude'] = this.latitude; + data['AppointmentNo'] = this.appointmentNo; + data['DischargeId'] = this.dischargeId; + data['StatusId'] = this.statusId; + data['ServiceId'] = this.serviceId; + data['Channel'] = this.channel; + if (this.orderpayment != null) { + data['orderpayment'] = this.orderpayment!.toJson(); + } + data['orderselectedservice'] = this.orderselectedservice; + + data['wforder'] = this.wforder; + data['orderapprovalobj'] = this.orderapprovalobj; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + data['StatusText'] = this.statusText; + data['PaymentStatus'] = this.paymentStatus; + data['ClientRequestid'] = this.clientRequestid; + data['PaymentStatusText'] = this.paymentStatusText; + data['ProjectName'] = this.projectName; + data['NearestProjectName'] = this.nearestProjectName; + data['PaymentAmount'] = this.paymentAmount; + if (this.wFOrder != null) { + data['WF_order'] = this.wFOrder!.toJson(); + } + data['ServiceText'] = this.serviceText; + data['isSentForApproval'] = this.isSentForApproval; + data['ExaCart_OrderId'] = this.exaCartOrderId; + data['isTimer'] = this.isTimer; + data['TimeSeconds'] = this.timeSeconds; + data['TotalPendingSeconds'] = this.totalPendingSeconds; + data['TimeMinute'] = this.timeMinute; + data['TimeHour'] = this.timeHour; + data['TimeTotalSeconds'] = this.timeTotalSeconds; + data['TimeTotalMinute'] = this.timeTotalMinute; + data['TimeTotalHour'] = this.timeTotalHour; + data['ApprovalStatus'] = this.approvalStatus; + data['isActive'] = this.isActive; + data['ClickButton'] = this.clickButton; + data['PickupLocation'] = this.pickupLocation; + data['DropOffLocation'] = this.dropOffLocation; + data['clinicName'] = this.clinicName; + data['DoctorName'] = this.doctorName; + data['Branch'] = this.branch; + data['Time'] = this.time; + data['Notes'] = this.notes; + return data; + } +} + +class Orderpayment { + int? iD; + int? orderId; + dynamic clientRequestId; + dynamic totalAmount; + int? paymentStatus; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + Orderpayment({this.iD, this.orderId, this.clientRequestId, this.totalAmount, this.paymentStatus, this.order, this.created, this.createdBy, this.modified, this.modifiedBy, this.isDeleted}); + + Orderpayment.fromJson(Map json) { + iD = json['ID']; + orderId = json['OrderId']; + clientRequestId = json['ClientRequestId']; + totalAmount = json['TotalAmount']; + paymentStatus = json['PaymentStatus']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = new Map(); + data['ID'] = this.iD; + data['OrderId'] = this.orderId; + data['ClientRequestId'] = this.clientRequestId; + data['TotalAmount'] = this.totalAmount; + data['PaymentStatus'] = this.paymentStatus; + data['Order'] = this.order; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + return data; + } +} + +class WFOrder { + dynamic wfButtonsDTO; + int? iD; + int? orderId; + int? previousStep; + int? nextStep; + int? serviceId; + dynamic order; + String? created; + dynamic createdBy; + dynamic modified; + dynamic modifiedBy; + bool? isDeleted; + + WFOrder({this.wfButtonsDTO, this.iD, this.orderId, this.previousStep, this.nextStep, this.serviceId, this.order, this.created, this.createdBy, this.modified, this.modifiedBy, this.isDeleted}); + + WFOrder.fromJson(Map json) { + wfButtonsDTO = json['wf_ButtonsDTO']; + iD = json['ID']; + orderId = json['OrderId']; + previousStep = json['PreviousStep']; + nextStep = json['NextStep']; + serviceId = json['ServiceId']; + order = json['Order']; + created = json['Created']; + createdBy = json['CreatedBy']; + modified = json['Modified']; + modifiedBy = json['ModifiedBy']; + isDeleted = json['IsDeleted']; + } + + Map toJson() { + final Map data = new Map(); + data['wf_ButtonsDTO'] = this.wfButtonsDTO; + data['ID'] = this.iD; + data['OrderId'] = this.orderId; + data['PreviousStep'] = this.previousStep; + data['NextStep'] = this.nextStep; + data['ServiceId'] = this.serviceId; + data['Order'] = this.order; + data['Created'] = this.created; + data['CreatedBy'] = this.createdBy; + data['Modified'] = this.modified; + data['ModifiedBy'] = this.modifiedBy; + data['IsDeleted'] = this.isDeleted; + return data; + } +} diff --git a/lib/features/prescriptions/prescriptions_repo.dart b/lib/features/prescriptions/prescriptions_repo.dart index 2e3f1aa..c6e7150 100644 --- a/lib/features/prescriptions/prescriptions_repo.dart +++ b/lib/features/prescriptions/prescriptions_repo.dart @@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/patient_prescriptions_response_model.dart'; +import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/prescription_delivery_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/prescription_detail_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -15,7 +16,13 @@ abstract class PrescriptionsRepo { Future>> getPrescriptionInstructionsPDF({required PatientPrescriptionsResponseModel prescriptionsResponseModel}); - Future>> getPrescriptionPDF({required PatientPrescriptionsResponseModel prescriptionsResponseModel, required List prescriptionDetailsList}); + Future>> getPrescriptionPDF( + {required PatientPrescriptionsResponseModel prescriptionsResponseModel, required List prescriptionDetailsList}); + + Future>> submitPrescriptionDeliveryRequest( + {required String latitude, required String longitude, required String appointmentNo, required String dischargeID, required String projectID}); + + Future>> getPrescriptionOrdersList(); } class PrescriptionsRepoImp implements PrescriptionsRepo { @@ -158,7 +165,8 @@ class PrescriptionsRepoImp implements PrescriptionsRepo { } @override - Future> getPrescriptionPDF({required PatientPrescriptionsResponseModel prescriptionsResponseModel, required List prescriptionDetailsList}) async { + Future> getPrescriptionPDF( + {required PatientPrescriptionsResponseModel prescriptionsResponseModel, required List prescriptionDetailsList}) async { Map mapDevice = { "AppointmentDate": prescriptionsResponseModel.appointmentDate, "ClinicName": prescriptionsResponseModel.clinicDescription, @@ -174,6 +182,7 @@ class PrescriptionsRepoImp implements PrescriptionsRepo { "To": Utils.appState.getAuthenticatedUser()!.emailAddress, "SetupID": prescriptionsResponseModel.setupID, "IsDownload": true, + "isDentalAllowedBackend": false, }; try { @@ -206,4 +215,84 @@ class PrescriptionsRepoImp implements PrescriptionsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> submitPrescriptionDeliveryRequest( + {required String latitude, required String longitude, required String appointmentNo, required String dischargeID, required String projectID}) async { + Map mapDevice = { + "latitude": latitude, + "longitude": longitude, + "AppointmentNo": appointmentNo, + "DischargeID": dischargeID, + "ProjectID": projectID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + isRCService: true, + ADD_PRESCRIPTION_ORDER_RC, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> getPrescriptionOrdersList() async { + Map mapDevice = {}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + isRCService: true, + GET_ALL_PRESCRIPTION_ORDERS_RC, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + final prescriptionOrders = response['response'].map((item) => PrescriptionDeliveryResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: prescriptionOrders, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index aac25c1..ff86406 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -1,8 +1,24 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/patient_prescriptions_response_model.dart'; +import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/prescription_delivery_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/prescription_detail_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_delivery_order_summary_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class PrescriptionsViewModel extends ChangeNotifier { bool isPrescriptionsOrdersLoading = false; @@ -10,6 +26,7 @@ class PrescriptionsViewModel extends ChangeNotifier { PrescriptionsRepo prescriptionsRepo; ErrorHandlerService errorHandlerService; + NavigationService navServices; // Prescription Orders Lists List patientPrescriptionOrders = []; @@ -27,19 +44,31 @@ class PrescriptionsViewModel extends ChangeNotifier { String prescriptionPDFBase64Data = ""; - PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService}); + late GeocodeResponse locationGeocodeResponse; + + bool isPrescriptionsDeliveryOrdersLoading = false; + List prescriptionsOrderList = []; + + PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices}); initPrescriptionsViewModel() { patientPrescriptionOrders.clear(); patientPrescriptionOrdersByClinic.clear(); patientPrescriptionOrdersByHospital.clear(); patientPrescriptionOrdersViewList.clear(); + prescriptionsOrderList.clear(); isPrescriptionsOrdersLoading = true; isSortByClinic = true; + isPrescriptionsDeliveryOrdersLoading = true; getPatientPrescriptionOrders(); notifyListeners(); } + + checkIfReminderExistForPrescription(int index) async { + prescriptionDetailsList[index].hasReminder = await CalenderUtilsNew.instance.checkIfEventExist(prescriptionDetailsList[index].itemID?.toString() ?? ""); + } + setPrescriptionsDetailsLoading() { isPrescriptionsDetailsLoading = true; prescriptionDetailsList.clear(); @@ -121,6 +150,9 @@ class PrescriptionsViewModel extends ChangeNotifier { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { prescriptionDetailsList = apiResponse.data!; + prescriptionDetailsList.forEach((element) async { + await checkIfReminderExistForPrescription(prescriptionDetailsList.indexOf(element)); + }); isPrescriptionsDetailsLoading = false; notifyListeners(); if (onSuccess != null) { @@ -173,4 +205,92 @@ class PrescriptionsViewModel extends ChangeNotifier { }, ); } + + showError(String errorMessage) { + getIt().showErrorBottomSheet(message: errorMessage); + } + + Future submitPrescriptionDeliveryRequest( + {required String latitude, + required String longitude, + required String appointmentNo, + required String dischargeID, + required String projectID, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + final result = await prescriptionsRepo.submitPrescriptionDeliveryRequest(latitude: latitude, longitude: longitude, appointmentNo: appointmentNo, dischargeID: dischargeID, projectID: projectID); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + void initiatePrescriptionDelivery() async { + getIt.get().getLocation( + isShowConfirmDialog: true, + onSuccess: (position) async { + bool result = await navServices.push( + CustomPageRoute( + page: MapUtilityScreen( + confirmButtonString: LocaleKeys.next.tr(), + titleString: "Select Location".needTranslation, + subTitleString: "Please select the location for prescription delivery".needTranslation, + isGmsAvailable: getIt.get().isGMSAvailable, + ), + direction: AxisDirection.down), + ); + print("Location Selected: $result"); + if (result) { + LocationViewModel locationViewModel = getIt.get(); + locationGeocodeResponse = locationViewModel.geocodeResponse!; + navServices.push( + CustomPageRoute( + page: PrescriptionDeliveryOrderSummaryPage(), + ), + ); + } + }); + } + + Future getPrescriptionOrdersList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isPrescriptionsDeliveryOrdersLoading = true; + prescriptionsOrderList.clear(); + notifyListeners(); + + final result = await prescriptionsRepo.getPrescriptionOrdersList(); + + result.fold( + (failure) async { + isPrescriptionsDeliveryOrdersLoading = false; + notifyListeners(); + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + isPrescriptionsDeliveryOrdersLoading = false; + notifyListeners(); + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + isPrescriptionsDeliveryOrdersLoading = false; + prescriptionsOrderList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/radiology/radiology_repo.dart b/lib/features/radiology/radiology_repo.dart index 0a44428..2af3e0f 100644 --- a/lib/features/radiology/radiology_repo.dart +++ b/lib/features/radiology/radiology_repo.dart @@ -9,11 +9,14 @@ import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patien import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class RadiologyRepo { - Future>>> getPatientRadiologyOrders({required String patientId}); + Future>>> getPatientRadiologyOrders(); Future>> getRadiologyImage({required PatientRadiologyResponseModel patientRadiologyResponseModel}); - Future>> getRadiologyReportPDF({required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser}); + Future>> getRadiologyReportPDF( + {required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser}); + + Future>>> getPatientRadiologyOrderByAppointment({required num appointmentNo, required num projectID}); } class RadiologyRepoImp implements RadiologyRepo { @@ -23,7 +26,7 @@ class RadiologyRepoImp implements RadiologyRepo { RadiologyRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientRadiologyOrders({required String patientId}) async { + Future>>> getPatientRadiologyOrders() async { Map mapDevice = {}; try { @@ -40,10 +43,16 @@ class RadiologyRepoImp implements RadiologyRepo { try { if (response['FinalRadiologyList'] != null && response['FinalRadiologyList'].length != 0) { final list = response['FinalRadiologyList']; - radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + radOrders = list + .map((item) => PatientRadiologyResponseModel.fromJson(item as Map)) + .toList() + .cast(); } else { final list = response['FinalRadiologyListAPI']; - radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + radOrders = list + .map((item) => PatientRadiologyResponseModel.fromJson(item as Map)) + .toList() + .cast(); } apiResponse = GenericApiModel>( @@ -107,7 +116,8 @@ class RadiologyRepoImp implements RadiologyRepo { } @override - Future>> getRadiologyReportPDF({required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser}) async { + Future>> getRadiologyReportPDF( + {required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser}) async { Map mapDevice = { "InvoiceNo": Utils.isVidaPlusProject(patientRadiologyResponseModel.projectID!) ? 0 : patientRadiologyResponseModel.invoiceNo, "InvoiceNo_VP": Utils.isVidaPlusProject(patientRadiologyResponseModel.projectID!) ? patientRadiologyResponseModel.invoiceNo : 0, @@ -121,7 +131,8 @@ class RadiologyRepoImp implements RadiologyRepo { 'ClinicName': patientRadiologyResponseModel.clinicDescription, 'DateofBirth': authenticatedUser.dateofBirth, 'DoctorName': patientRadiologyResponseModel.doctorName, - 'OrderDate': '${patientRadiologyResponseModel.orderDate!.year}-${patientRadiologyResponseModel.orderDate!.month}-${patientRadiologyResponseModel.orderDate!.day}', + 'OrderDate': + '${patientRadiologyResponseModel.orderDate!.year}-${patientRadiologyResponseModel.orderDate!.month}-${patientRadiologyResponseModel.orderDate!.day}', 'PatientIditificationNum': authenticatedUser.patientIdentificationNo, 'PatientMobileNumber': authenticatedUser.mobileNumber, 'PatientName': "${authenticatedUser.firstName!} ${authenticatedUser.lastName!}", @@ -159,4 +170,50 @@ class RadiologyRepoImp implements RadiologyRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientRadiologyOrderByAppointment({required num appointmentNo, required num projectID}) async { + Map mapDevice = { + "AppointmentNo": appointmentNo, + "ProjectID": projectID, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_PATIENT_ORDERS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + final radOrders; + try { + if (response['FinalRadiologyList'] != null && response['FinalRadiologyList'].length != 0) { + final list = response['FinalRadiologyList']; + radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + } else { + final list = response['FinalRadiologyListAPI']; + radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: radOrders, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/radiology/radiology_view_model.dart b/lib/features/radiology/radiology_view_model.dart index 3441881..986945e 100644 --- a/lib/features/radiology/radiology_view_model.dart +++ b/lib/features/radiology/radiology_view_model.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; +import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'models/resp_models/patient_radiology_response_model.dart'; @@ -11,16 +15,37 @@ class RadiologyViewModel extends ChangeNotifier { RadiologyRepo radiologyRepo; ErrorHandlerService errorHandlerService; + NavigationService navigationService; List patientRadiologyOrders = []; - + List filteredRadiologyOrders = []; + List tempRadiologyOrders = []; String radiologyImageURL = ""; String patientRadiologyReportPDFBase64 = ""; - RadiologyViewModel({required this.radiologyRepo, required this.errorHandlerService}); + late List _radiologySuggestionsList = []; + + List get radiologySuggestions => _radiologySuggestionsList; + + late PatientRadiologyResponseModel patientRadiologyOrderByAppointment; + + // --- Grouping fields: By Clinic / By Hospital --- + bool isSortByClinic = true; + List> patientRadiologyOrdersByClinic = []; + List> patientRadiologyOrdersByHospital = []; + List> patientRadiologyOrdersViewList = []; + + void setIsSortByClinic(bool value) { + isSortByClinic = value; + patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital; + notifyListeners(); + } + + RadiologyViewModel({required this.radiologyRepo, required this.errorHandlerService, required this.navigationService}); - initRadiologyProvider() { + initRadiologyViewModel() { patientRadiologyOrders.clear(); + filteredRadiologyOrders.clear(); isRadiologyOrdersLoading = true; isRadiologyPDFReportLoading = true; radiologyImageURL = ""; @@ -29,7 +54,7 @@ class RadiologyViewModel extends ChangeNotifier { } Future getPatientRadiologyOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await radiologyRepo.getPatientRadiologyOrders(patientId: "1231755"); + final result = await radiologyRepo.getPatientRadiologyOrders(); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -38,7 +63,10 @@ class RadiologyViewModel extends ChangeNotifier { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { patientRadiologyOrders = apiResponse.data!; + filteredRadiologyOrders = List.from(patientRadiologyOrders); + tempRadiologyOrders = [...patientRadiologyOrders]; isRadiologyOrdersLoading = false; + filterSuggestions(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -48,7 +76,37 @@ class RadiologyViewModel extends ChangeNotifier { ); } - Future getRadiologyImage({required PatientRadiologyResponseModel patientRadiologyResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPatientRadiologyOrdersByAppointment({required num appointmentNo, required num projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await radiologyRepo.getPatientRadiologyOrderByAppointment(appointmentNo: appointmentNo, projectID: projectID); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (apiResponse.data!.isNotEmpty) { + if (onSuccess != null) { + onSuccess(apiResponse); + } + navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: apiResponse.data!.first), + ), + ); + } else { + if (onError != null) { + onError("No Radiology Orders Found".needTranslation); + } + } + } + }, + ); + } + + Future getRadiologyImage( + {required PatientRadiologyResponseModel patientRadiologyResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await radiologyRepo.getRadiologyImage(patientRadiologyResponseModel: patientRadiologyResponseModel); result.fold( @@ -68,8 +126,12 @@ class RadiologyViewModel extends ChangeNotifier { } Future getRadiologyPDF( - {required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser, Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await radiologyRepo.getRadiologyReportPDF(patientRadiologyResponseModel: patientRadiologyResponseModel, authenticatedUser: authenticatedUser); + {required PatientRadiologyResponseModel patientRadiologyResponseModel, + required AuthenticatedUser authenticatedUser, + Function(dynamic)? onSuccess, + Function(String)? onError}) async { + final result = + await radiologyRepo.getRadiologyReportPDF(patientRadiologyResponseModel: patientRadiologyResponseModel, authenticatedUser: authenticatedUser); result.fold( (failure) async => await errorHandlerService.handleError( @@ -93,4 +155,20 @@ class RadiologyViewModel extends ChangeNotifier { }, ); } + + filterSuggestions() { + final List labels = patientRadiologyOrders.map((detail) => detail.description).whereType().toList(); + _radiologySuggestionsList = labels.toSet().toList(); + notifyListeners(); + } + + filterRadiologyReports(String query) { + if (query.isEmpty) { + patientRadiologyOrders = tempRadiologyOrders; // reset + } else { + filteredRadiologyOrders = filteredRadiologyOrders.where((desc) => desc.description!.toLowerCase().contains(query.toLowerCase())).toList(); + patientRadiologyOrders = filteredRadiologyOrders; + } + notifyListeners(); + } } diff --git a/lib/features/smartwatch_health_data/health_provider.dart b/lib/features/smartwatch_health_data/health_provider.dart new file mode 100644 index 0000000..fc9dacc --- /dev/null +++ b/lib/features/smartwatch_health_data/health_provider.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_service.dart'; + +class HealthProvider with ChangeNotifier { + final HealthService _healthService = HealthService(); + Map> healthData = {}; + bool isLoading = false; + String? error; + String selectedTimeRange = '7D'; + int selectedTabIndex = 0; + + void onTabChanged(int index) { + selectedTabIndex = index; + notifyListeners(); + } + + Future fetchHealthData() async { + isLoading = true; + error = null; + notifyListeners(); + healthData.clear(); + try { + final authorized = await _healthService.requestAuthorization(); + if (!authorized) { + error = 'Health data access not authorized'; + isLoading = false; + notifyListeners(); + return; + } + + final startTime = _getStartDate(); + final endTime = DateTime.now(); + + healthData = await _healthService.getAllHealthData(startTime, endTime); + + isLoading = false; + notifyListeners(); + } catch (e) { + error = 'Error fetching health data: $e'; + isLoading = false; + notifyListeners(); + } + } + + Future refreshMetric(HealthDataType type) async { + try { + final startTime = _getStartDate(); + final endTime = DateTime.now(); + + final data = await _healthService.getSpecificHealthData( + type, + startTime, + endTime, + ); + + healthData[type] = data; + notifyListeners(); + } catch (e) { + print('Error refreshing metric $type: $e'); + } + } + + void updateTimeRange(String range) { + selectedTimeRange = range; + fetchHealthData(); + } + + DateTime _getStartDate() { + switch (selectedTimeRange) { + case '1D': + return DateTime.now().subtract(const Duration(days: 1)); + case '7D': + return DateTime.now().subtract(const Duration(days: 7)); + case '1M': + return DateTime.now().subtract(const Duration(days: 30)); + case '3M': + return DateTime.now().subtract(const Duration(days: 90)); + case '1Y': + return DateTime.now().subtract(const Duration(days: 365)); + default: + return DateTime.now().subtract(const Duration(days: 7)); + } + } +} diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart new file mode 100644 index 0000000..d3815b3 --- /dev/null +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:health/health.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'health_utils.dart'; + +class HealthService { + static final HealthService _instance = HealthService._internal(); + + factory HealthService() => _instance; + + HealthService._internal(); + + final Health health = Health(); + + final List _healthMetrics = [ + HealthDataType.HEART_RATE, + // HealthDataType.STEPS, + // HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + // HealthDataType.DISTANCE_WALKING_RUNNING, + // HealthDataType.ACTIVE_ENERGY_BURNED, + ]; + + final List _healthMetricsAndroid = [ + HealthDataType.HEART_RATE, + HealthDataType.STEPS, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + HealthDataType.DISTANCE_DELTA, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + final List _healthPermissions = [ + HealthDataType.HEART_RATE, + HealthDataType.STEPS, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + Platform.isAndroid ? HealthDataType.DISTANCE_DELTA : HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + final List _healthMetricsCumulative = [ + HealthDataType.BLOOD_OXYGEN, + HealthDataType.STEPS, + Platform.isAndroid ? HealthDataType.DISTANCE_DELTA : HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + Future requestAuthorization() async { + try { + final types = (Platform.isAndroid) ? dataTypesAndroid : _healthPermissions; + final granted = await health.requestAuthorization(types); + await Permission.activityRecognition.request(); + await Permission.location.request(); + // request access to read historic data + await Health().requestHealthDataHistoryAuthorization(); + // await authorize(); + return granted; + } catch (e) { + print('Authorization error: $e'); + return false; + } + } + + Future>> getAllHealthData( + DateTime startTime, + DateTime endTime, + ) async { + final Map> allData = {}; + + try { + for (var type in _healthMetricsCumulative) { + try { + final data = await health.getHealthIntervalDataFromTypes( + startDate: startTime, + endDate: endTime, + types: [type], + interval: 86400, + ); + + if (type == HealthDataType.BLOOD_OXYGEN) { + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + point.value = NumericHealthValue( + numericValue: numericValue * 100, + ); + } + } + } + + if (type == HealthDataType.DISTANCE_WALKING_RUNNING) { + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + point.value = NumericHealthValue( + numericValue: numericValue / 1000, + ); + } + } + } + + allData[type] = data; + } catch (e) { + print('Error fetching $type: $e'); + allData[type] = []; + } + } + + for (var type in Platform.isIOS ? _healthMetrics : _healthMetricsAndroid) { + try { + final data = await health.getHealthDataFromTypes( + startTime: startTime, + endTime: endTime, + types: [type], + // includeManualEntry: false + // interval: 86400, + ); + + allData[type] = data; + } catch (e) { + print('Error fetching $type: $e'); + allData[type] = []; + } + } + } catch (e) { + print('Error fetching health data: $e'); + } + + return allData; + } + + Future> getSpecificHealthData( + HealthDataType type, + DateTime startTime, + DateTime endTime, + ) async { + try { + final data = await health.getHealthDataFromTypes( + startTime: startTime, + endTime: endTime, + types: [type], + ); + return data; + } catch (e) { + print('Error fetching $type: $e'); + return []; + } + } +} diff --git a/lib/features/smartwatch_health_data/health_utils.dart b/lib/features/smartwatch_health_data/health_utils.dart new file mode 100644 index 0000000..3d3588e --- /dev/null +++ b/lib/features/smartwatch_health_data/health_utils.dart @@ -0,0 +1,109 @@ +import 'package:health/health.dart'; + +/// Data types available on iOS via Apple Health. +const List dataTypesIOS = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.APPLE_STAND_TIME, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_MOVE_TIME, + HealthDataType.AUDIOGRAM, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.BODY_MASS_INDEX, + HealthDataType.BODY_TEMPERATURE, + HealthDataType.DIETARY_CARBS_CONSUMED, + HealthDataType.DIETARY_CAFFEINE, + HealthDataType.DIETARY_ENERGY_CONSUMED, + HealthDataType.DIETARY_FATS_CONSUMED, + HealthDataType.DIETARY_PROTEIN_CONSUMED, + HealthDataType.ELECTRODERMAL_ACTIVITY, + HealthDataType.FORCED_EXPIRATORY_VOLUME, + HealthDataType.HEART_RATE, + HealthDataType.HEART_RATE_VARIABILITY_SDNN, + HealthDataType.HEIGHT, + HealthDataType.INSULIN_DELIVERY, + HealthDataType.RESPIRATORY_RATE, + HealthDataType.PERIPHERAL_PERFUSION_INDEX, + HealthDataType.STEPS, + HealthDataType.WAIST_CIRCUMFERENCE, + HealthDataType.WEIGHT, + HealthDataType.FLIGHTS_CLIMBED, + HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.WALKING_SPEED, + HealthDataType.MINDFULNESS, + HealthDataType.SLEEP_AWAKE, + HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_REM, + HealthDataType.WATER, + HealthDataType.EXERCISE_TIME, + HealthDataType.WORKOUT, + HealthDataType.HEADACHE_NOT_PRESENT, + HealthDataType.HEADACHE_MILD, + HealthDataType.HEADACHE_MODERATE, + HealthDataType.HEADACHE_SEVERE, + HealthDataType.HEADACHE_UNSPECIFIED, + HealthDataType.LEAN_BODY_MASS, + + // note that a phone cannot write these ECG-based types - only read them + // HealthDataType.ELECTROCARDIOGRAM, + // HealthDataType.HIGH_HEART_RATE_EVENT, + // HealthDataType.IRREGULAR_HEART_RATE_EVENT, + // HealthDataType.LOW_HEART_RATE_EVENT, + // HealthDataType.RESTING_HEART_RATE, + // HealthDataType.WALKING_HEART_RATE, + // HealthDataType.ATRIAL_FIBRILLATION_BURDEN, + + HealthDataType.NUTRITION, + HealthDataType.GENDER, + HealthDataType.BLOOD_TYPE, + HealthDataType.BIRTH_DATE, + HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, + HealthDataType.UV_INDEX, +]; + +/// Data types available on Android via the Google Health Connect API. +const List dataTypesAndroid = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + // HealthDataType.BASAL_ENERGY_BURNED, + // HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BODY_FAT_PERCENTAGE, + // HealthDataType.HEIGHT, + // HealthDataType.WEIGHT, + // HealthDataType.LEAN_BODY_MASS, + // HealthDataType.BODY_MASS_INDEX, + // HealthDataType.BODY_TEMPERATURE, + HealthDataType.HEART_RATE, + // HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + HealthDataType.STEPS, + HealthDataType.DISTANCE_DELTA, + // HealthDataType.SPEED, + HealthDataType.RESPIRATORY_RATE, + // HealthDataType.SLEEP_ASLEEP, + // HealthDataType.SLEEP_AWAKE_IN_BED, + // HealthDataType.SLEEP_AWAKE, + // HealthDataType.SLEEP_DEEP, + // HealthDataType.SLEEP_LIGHT, + // HealthDataType.SLEEP_OUT_OF_BED, + // HealthDataType.SLEEP_REM, + // HealthDataType.SLEEP_UNKNOWN, + // HealthDataType.SLEEP_SESSION, + // HealthDataType.WATER, + // HealthDataType.WORKOUT, + HealthDataType.RESTING_HEART_RATE, + // HealthDataType.FLIGHTS_CLIMBED, + // HealthDataType.NUTRITION, + HealthDataType.TOTAL_CALORIES_BURNED, + // HealthDataType.MENSTRUATION_FLOW, +]; \ No newline at end of file diff --git a/lib/features/symptoms_checker/data/organ_mapping_data.dart b/lib/features/symptoms_checker/data/organ_mapping_data.dart new file mode 100644 index 0000000..6b50b25 --- /dev/null +++ b/lib/features/symptoms_checker/data/organ_mapping_data.dart @@ -0,0 +1,422 @@ +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; + +class OrganData { + // ==================== FRONT VIEW ==================== + static const List frontViewOrgans = [ + // ========== HEAD & FACE ========== + OrganModel( + id: 'head', + name: 'whole_head', + description: 'Head', + bodyView: BodyView.front, + position: OrganPosition(x: 0.50, y: 0.03), + ), + OrganModel( + id: 'left_eye', + name: 'eyes', + description: 'Left Eye', + bodyView: BodyView.front, + position: OrganPosition(x: 0.43, y: 0.07), + ), + OrganModel( + id: 'right_eye', + name: 'eyes', + description: 'Right Eye', + bodyView: BodyView.front, + position: OrganPosition(x: 0.57, y: 0.07), + ), + OrganModel( + id: 'nose_mouth', + name: 'oral_cavity', + description: 'Nose/Mouth', + bodyView: BodyView.front, + position: OrganPosition(x: 0.50, y: 0.09), + ), + + // ========== NECK ========== + OrganModel( + id: 'throat', + name: 'neck_or_throat', + description: 'Throat', + bodyView: BodyView.front, + position: OrganPosition(x: 0.50, y: 0.15), + ), + + // ========== SHOULDERS ========== + OrganModel( + id: 'left_shoulder', + name: 'upper_arm', + description: 'Left Shoulder', + bodyView: BodyView.front, + position: OrganPosition(x: 0.22, y: 0.22), + ), + OrganModel( + id: 'right_shoulder', + name: 'upper_arm', + description: 'Right Shoulder', + bodyView: BodyView.front, + position: OrganPosition(x: 0.8, y: 0.22), + ), + + // ========== CHEST ========== + OrganModel( + id: 'left_chest', + name: 'chest', + description: 'Left Chest', + bodyView: BodyView.front, + position: OrganPosition(x: 0.38, y: 0.22), + ), + OrganModel( + id: 'center_chest', + name: 'chest', + description: 'Center Chest', + bodyView: BodyView.front, + position: OrganPosition(x: 0.52, y: 0.25), + ), + OrganModel( + id: 'right_chest', + name: 'chest', + description: 'Right Chest', + bodyView: BodyView.front, + position: OrganPosition(x: 0.65, y: 0.22), + ), + + // ========== RIBS ========== + OrganModel( + id: 'left_ribs', + name: 'chest', + description: 'Left Ribs', + bodyView: BodyView.front, + position: OrganPosition(x: 0.38, y: 0.35), + ), + OrganModel( + id: 'right_ribs', + name: 'chest', + description: 'Right Ribs', + bodyView: BodyView.front, + position: OrganPosition(x: 0.64, y: 0.35), + ), + + // ========== ABDOMEN ========== + OrganModel( + id: 'upper_abdomen', + name: 'upper_abdomen', + description: 'Upper Abdomen', + bodyView: BodyView.front, + position: OrganPosition(x: 0.51, y: 0.31), + ), + OrganModel( + id: 'navel', + name: 'mid_abdomen', + description: 'Navel', + bodyView: BodyView.front, + position: OrganPosition(x: 0.51, y: 0.38), + ), + OrganModel( + id: 'lower_abdomen', + name: 'lower_abdomen', + description: 'Lower Abdomen', + bodyView: BodyView.front, + position: OrganPosition(x: 0.51, y: 0.44), + ), + + // ========== PELVIS ========== + OrganModel( + id: 'left_groin', + name: 'sexual_organs', + description: 'Left Groin', + bodyView: BodyView.front, + position: OrganPosition(x: 0.42, y: 0.49), + ), + OrganModel( + id: 'right_groin', + name: 'sexual_organs', + description: 'Right Groin', + bodyView: BodyView.front, + position: OrganPosition(x: 0.59, y: 0.49), + ), + + // ========== LEFT ARM ========== + + OrganModel( + id: 'left_elbow', + name: 'elbow', + description: 'Left Elbow', + bodyView: BodyView.front, + position: OrganPosition(x: 0.21, y: 0.35), + ), + OrganModel( + id: 'left_forearm', + name: 'forearm', + description: 'Left Forearm', + bodyView: BodyView.front, + position: OrganPosition(x: 0.19, y: 0.43), + ), + OrganModel( + id: 'left_wrist', + name: 'hand', + description: 'Left Wrist', + bodyView: BodyView.front, + position: OrganPosition(x: 0.14, y: 0.5), + ), + + // ========== RIGHT ARM ========== + + OrganModel( + id: 'right_elbow', + name: 'elbow', + description: 'Right Elbow', + bodyView: BodyView.front, + position: OrganPosition(x: 0.79, y: 0.35), + ), + OrganModel( + id: 'right_forearm', + name: 'forearm', + description: 'Right Forearm', + bodyView: BodyView.front, + position: OrganPosition(x: 0.81, y: 0.43), + ), + OrganModel( + id: 'right_wrist', + name: 'hand', + description: 'Right Wrist', + bodyView: BodyView.front, + position: OrganPosition(x: 0.85, y: 0.5), + ), + + // ========== LEFT LEG ========== + OrganModel( + id: 'left_thigh', + name: 'thigh', + description: 'Left Thigh', + bodyView: BodyView.front, + position: OrganPosition(x: 0.4, y: 0.60), + ), + OrganModel( + id: 'left_knee', + name: 'knee', + description: 'Left Knee', + bodyView: BodyView.front, + position: OrganPosition(x: 0.4, y: 0.72), + ), + OrganModel( + id: 'left_shin', + name: 'lower_leg', + description: 'Left Shin', + bodyView: BodyView.front, + position: OrganPosition(x: 0.41, y: 0.82), + ), + OrganModel( + id: 'left_ankle', + name: 'foot', + description: 'Left Ankle', + bodyView: BodyView.front, + position: OrganPosition(x: 0.43, y: 0.91), + ), + + // ========== RIGHT LEG ========== + OrganModel( + id: 'right_thigh', + name: 'thigh', + description: 'Right Thigh', + bodyView: BodyView.front, + position: OrganPosition(x: 0.65, y: 0.60), + ), + OrganModel( + id: 'right_knee', + name: 'knee', + description: 'Right Knee', + bodyView: BodyView.front, + position: OrganPosition(x: 0.6, y: 0.72), + ), + OrganModel( + id: 'right_shin', + name: 'lower_leg', + description: 'Right Shin', + bodyView: BodyView.front, + position: OrganPosition(x: 0.57, y: 0.82), + ), + OrganModel( + id: 'right_ankle', + name: 'foot', + description: 'Right Ankle', + bodyView: BodyView.front, + position: OrganPosition(x: 0.57, y: 0.91), + ), + ]; + + // ==================== BACK VIEW ==================== + static const List backViewOrgans = [ + // ========== HEAD & NECK ========== + OrganModel( + id: 'back_of_head', + name: 'whole_head', + description: 'Back of Head', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.04), + ), + OrganModel( + id: 'neck_back', + name: 'nape_of_neck', + description: 'Neck', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.14), + ), + + // ========== SHOULDERS ========== + OrganModel( + id: 'left_shoulder_back', + name: 'upper_arm', + description: 'Left Shoulder', + bodyView: BodyView.back, + position: OrganPosition(x: 0.33, y: 0.19), + ), + OrganModel( + id: 'right_shoulder_back', + name: 'upper_arm', + description: 'Right Shoulder', + bodyView: BodyView.back, + position: OrganPosition(x: 0.67, y: 0.19), + ), + + // ========== UPPER BACK ========== + OrganModel( + id: 'upper_spine', + name: 'back', + description: 'Upper Spine', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.23), + ), + OrganModel( + id: 'left_upper_back', + name: 'back', + description: 'Left Upper Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.38, y: 0.28), + ), + OrganModel( + id: 'right_upper_back', + name: 'back', + description: 'Right Upper Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.62, y: 0.28), + ), + + // ========== MID BACK ========== + OrganModel( + id: 'mid_spine', + name: 'back', + description: 'Mid Spine', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.35), + ), + OrganModel( + id: 'left_mid_back', + name: 'back', + description: 'Left Mid Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.38, y: 0.35), + ), + OrganModel( + id: 'right_mid_back', + name: 'back', + description: 'Right Mid Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.62, y: 0.35), + ), + + // ========== LOWER BACK ========== + OrganModel( + id: 'lower_spine', + name: 'lower_back', + description: 'Lower Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.43), + ), + OrganModel( + id: 'left_lower_back', + name: 'lower_back', + description: 'Left Lower Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.40, y: 0.43), + ), + OrganModel( + id: 'right_lower_back', + name: 'lower_back', + description: 'Right Lower Back', + bodyView: BodyView.back, + position: OrganPosition(x: 0.60, y: 0.43), + ), + + // ========== GLUTES ========== + OrganModel( + id: 'sacrum', + name: 'lower_back', + description: 'Sacrum', + bodyView: BodyView.back, + position: OrganPosition(x: 0.50, y: 0.52), + ), + OrganModel( + id: 'left_glute', + name: 'buttocks', + description: 'Left Glute', + bodyView: BodyView.back, + position: OrganPosition(x: 0.45, y: 0.56), + ), + OrganModel( + id: 'right_glute', + name: 'buttocks', + description: 'Right Glute', + bodyView: BodyView.back, + position: OrganPosition(x: 0.55, y: 0.56), + ), + + // ========== LEGS ========== + OrganModel( + id: 'left_hamstring', + name: 'thigh', + description: 'Left Hamstring', + bodyView: BodyView.back, + position: OrganPosition(x: 0.43, y: 0.65), + ), + OrganModel( + id: 'right_hamstring', + name: 'thigh', + description: 'Right Hamstring', + bodyView: BodyView.back, + position: OrganPosition(x: 0.57, y: 0.65), + ), + OrganModel( + id: 'left_calf', + name: 'lower_leg', + description: 'Left Calf', + bodyView: BodyView.back, + position: OrganPosition(x: 0.43, y: 0.79), + ), + OrganModel( + id: 'right_calf', + name: 'lower_leg', + description: 'Right Calf', + bodyView: BodyView.back, + position: OrganPosition(x: 0.57, y: 0.79), + ), + ]; + + // Helper methods + static List getOrgansForView(BodyView view) { + return view == BodyView.front ? frontViewOrgans : backViewOrgans; + } + + static List getAllOrgans() { + return [...frontViewOrgans, ...backViewOrgans]; + } + + static OrganModel? getOrganById(String id) { + try { + return getAllOrgans().firstWhere((organ) => organ.id == id); + } catch (e) { + return null; + } + } +} diff --git a/lib/features/symptoms_checker/data/symptoms_mapping_data.dart b/lib/features/symptoms_checker/data/symptoms_mapping_data.dart new file mode 100644 index 0000000..65aa152 --- /dev/null +++ b/lib/features/symptoms_checker/data/symptoms_mapping_data.dart @@ -0,0 +1,323 @@ +import 'package:hmg_patient_app_new/features/symptoms_checker/models/symptom_model.dart'; + +class SymptomsMappingData { + // Map of organ IDs to their possible symptoms + static final Map> organSymptomsMap = { + // HEAD & FACE + 'head': [ + SymptomModel(id: 'head_s1', name: 'Headache', organId: 'head'), + SymptomModel(id: 'head_s2', name: 'Dizziness', organId: 'head'), + SymptomModel(id: 'head_s3', name: 'Migraine', organId: 'head'), + SymptomModel(id: 'head_s4', name: 'Head pressure', organId: 'head'), + SymptomModel(id: 'head_s5', name: 'Vertigo', organId: 'head'), + SymptomModel(id: 'head_s6', name: 'Confusion', organId: 'head'), + SymptomModel(id: 'head_s7', name: 'Memory problems', organId: 'head'), + ], + 'left_eye': [ + SymptomModel(id: 'left_eye_s1', name: 'Blurred vision', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s2', name: 'Eye pain', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s3', name: 'Redness', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s4', name: 'Itching', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s5', name: 'Watering', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s6', name: 'Sensitivity to light', organId: 'left_eye'), + SymptomModel(id: 'left_eye_s7', name: 'Double vision', organId: 'left_eye'), + ], + 'right_eye': [ + SymptomModel(id: 'right_eye_s1', name: 'Blurred vision', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s2', name: 'Eye pain', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s3', name: 'Redness', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s4', name: 'Itching', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s5', name: 'Watering', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s6', name: 'Sensitivity to light', organId: 'right_eye'), + SymptomModel(id: 'right_eye_s7', name: 'Double vision', organId: 'right_eye'), + ], + 'nose_mouth': [ + SymptomModel(id: 'nose_mouth_s1', name: 'Nasal congestion', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s2', name: 'Runny nose', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s3', name: 'Nosebleed', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s4', name: 'Loss of smell', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s5', name: 'Mouth pain', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s6', name: 'Dry mouth', organId: 'nose_mouth'), + SymptomModel(id: 'nose_mouth_s7', name: 'Difficulty swallowing', organId: 'nose_mouth'), + ], + + // NECK & THROAT + 'throat': [ + SymptomModel(id: 'throat_s1', name: 'Sore throat', organId: 'throat'), + SymptomModel(id: 'throat_s2', name: 'Difficulty swallowing', organId: 'throat'), + SymptomModel(id: 'throat_s3', name: 'Hoarseness', organId: 'throat'), + SymptomModel(id: 'throat_s4', name: 'Throat pain', organId: 'throat'), + SymptomModel(id: 'throat_s5', name: 'Swollen glands', organId: 'throat'), + SymptomModel(id: 'throat_s6', name: 'Cough', organId: 'throat'), + SymptomModel(id: 'throat_s7', name: 'Difficulty breathing', organId: 'throat'), + ], + + // SHOULDERS + 'left_shoulder': [ + SymptomModel(id: 'left_shoulder_s1', name: 'Shoulder pain', organId: 'left_shoulder'), + SymptomModel(id: 'left_shoulder_s2', name: 'Stiffness', organId: 'left_shoulder'), + SymptomModel(id: 'left_shoulder_s3', name: 'Limited mobility', organId: 'left_shoulder'), + SymptomModel(id: 'left_shoulder_s4', name: 'Swelling', organId: 'left_shoulder'), + SymptomModel(id: 'left_shoulder_s5', name: 'Weakness', organId: 'left_shoulder'), + SymptomModel(id: 'left_shoulder_s6', name: 'Clicking sound', organId: 'left_shoulder'), + ], + 'right_shoulder': [ + SymptomModel(id: 'right_shoulder_s1', name: 'Shoulder pain', organId: 'right_shoulder'), + SymptomModel(id: 'right_shoulder_s2', name: 'Stiffness', organId: 'right_shoulder'), + SymptomModel(id: 'right_shoulder_s3', name: 'Limited mobility', organId: 'right_shoulder'), + SymptomModel(id: 'right_shoulder_s4', name: 'Swelling', organId: 'right_shoulder'), + SymptomModel(id: 'right_shoulder_s5', name: 'Weakness', organId: 'right_shoulder'), + SymptomModel(id: 'right_shoulder_s6', name: 'Clicking sound', organId: 'right_shoulder'), + ], + + // CHEST + 'left_chest': [ + SymptomModel(id: 'left_chest_s1', name: 'Chest pain', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s2', name: 'Tightness', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s3', name: 'Pressure', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s4', name: 'Sharp pain', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s5', name: 'Burning sensation', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s6', name: 'Palpitations', organId: 'left_chest'), + SymptomModel(id: 'left_chest_s7', name: 'Shortness of breath', organId: 'left_chest'), + ], + 'center_chest': [ + SymptomModel(id: 'center_chest_s1', name: 'Chest pain', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s2', name: 'Tightness', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s3', name: 'Pressure', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s4', name: 'Sharp pain', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s5', name: 'Burning sensation', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s6', name: 'Heartburn', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s7', name: 'Difficulty breathing', organId: 'center_chest'), + SymptomModel(id: 'center_chest_s8', name: 'Palpitations', organId: 'center_chest'), + ], + 'right_chest': [ + SymptomModel(id: 'right_chest_s1', name: 'Chest pain', organId: 'right_chest'), + SymptomModel(id: 'right_chest_s2', name: 'Tightness', organId: 'right_chest'), + SymptomModel(id: 'right_chest_s3', name: 'Pressure', organId: 'right_chest'), + SymptomModel(id: 'right_chest_s4', name: 'Sharp pain', organId: 'right_chest'), + SymptomModel(id: 'right_chest_s5', name: 'Burning sensation', organId: 'right_chest'), + SymptomModel(id: 'right_chest_s6', name: 'Shortness of breath', organId: 'right_chest'), + ], + + // RIBS + 'left_ribs': [ + SymptomModel(id: 'left_ribs_s1', name: 'Rib pain', organId: 'left_ribs'), + SymptomModel(id: 'left_ribs_s2', name: 'Pain when breathing', organId: 'left_ribs'), + SymptomModel(id: 'left_ribs_s3', name: 'Tenderness', organId: 'left_ribs'), + SymptomModel(id: 'left_ribs_s4', name: 'Bruising', organId: 'left_ribs'), + SymptomModel(id: 'left_ribs_s5', name: 'Difficulty moving', organId: 'left_ribs'), + ], + 'right_ribs': [ + SymptomModel(id: 'right_ribs_s1', name: 'Rib pain', organId: 'right_ribs'), + SymptomModel(id: 'right_ribs_s2', name: 'Pain when breathing', organId: 'right_ribs'), + SymptomModel(id: 'right_ribs_s3', name: 'Tenderness', organId: 'right_ribs'), + SymptomModel(id: 'right_ribs_s4', name: 'Bruising', organId: 'right_ribs'), + SymptomModel(id: 'right_ribs_s5', name: 'Difficulty moving', organId: 'right_ribs'), + ], + + // ABDOMEN + 'upper_abdomen': [ + SymptomModel(id: 'upper_abdomen_s1', name: 'Abdominal pain', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s2', name: 'Bloating', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s3', name: 'Nausea', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s4', name: 'Vomiting', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s5', name: 'Heartburn', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s6', name: 'Loss of appetite', organId: 'upper_abdomen'), + SymptomModel(id: 'upper_abdomen_s7', name: 'Indigestion', organId: 'upper_abdomen'), + ], + 'navel': [ + SymptomModel(id: 'navel_s1', name: 'Abdominal pain', organId: 'navel'), + SymptomModel(id: 'navel_s2', name: 'Cramping', organId: 'navel'), + SymptomModel(id: 'navel_s3', name: 'Bloating', organId: 'navel'), + SymptomModel(id: 'navel_s4', name: 'Gas', organId: 'navel'), + SymptomModel(id: 'navel_s5', name: 'Tenderness', organId: 'navel'), + ], + 'lower_abdomen': [ + SymptomModel(id: 'lower_abdomen_s1', name: 'Lower abdominal pain', organId: 'lower_abdomen'), + SymptomModel(id: 'lower_abdomen_s2', name: 'Cramping', organId: 'lower_abdomen'), + SymptomModel(id: 'lower_abdomen_s3', name: 'Bloating', organId: 'lower_abdomen'), + SymptomModel(id: 'lower_abdomen_s4', name: 'Constipation', organId: 'lower_abdomen'), + SymptomModel(id: 'lower_abdomen_s5', name: 'Diarrhea', organId: 'lower_abdomen'), + SymptomModel(id: 'lower_abdomen_s6', name: 'Urinary discomfort', organId: 'lower_abdomen'), + ], + + // PELVIS & GROIN + 'left_groin': [ + SymptomModel(id: 'left_groin_s1', name: 'Groin pain', organId: 'left_groin'), + SymptomModel(id: 'left_groin_s2', name: 'Swelling', organId: 'left_groin'), + SymptomModel(id: 'left_groin_s3', name: 'Tenderness', organId: 'left_groin'), + SymptomModel(id: 'left_groin_s4', name: 'Limited mobility', organId: 'left_groin'), + ], + 'right_groin': [ + SymptomModel(id: 'right_groin_s1', name: 'Groin pain', organId: 'right_groin'), + SymptomModel(id: 'right_groin_s2', name: 'Swelling', organId: 'right_groin'), + SymptomModel(id: 'right_groin_s3', name: 'Tenderness', organId: 'right_groin'), + SymptomModel(id: 'right_groin_s4', name: 'Limited mobility', organId: 'right_groin'), + ], + + // ARMS - LEFT + 'left_elbow': [ + SymptomModel(id: 'left_elbow_s1', name: 'Elbow pain', organId: 'left_elbow'), + SymptomModel(id: 'left_elbow_s2', name: 'Stiffness', organId: 'left_elbow'), + SymptomModel(id: 'left_elbow_s3', name: 'Swelling', organId: 'left_elbow'), + SymptomModel(id: 'left_elbow_s4', name: 'Limited mobility', organId: 'left_elbow'), + SymptomModel(id: 'left_elbow_s5', name: 'Tingling', organId: 'left_elbow'), + ], + 'left_forearm': [ + SymptomModel(id: 'left_forearm_s1', name: 'Forearm pain', organId: 'left_forearm'), + SymptomModel(id: 'left_forearm_s2', name: 'Weakness', organId: 'left_forearm'), + SymptomModel(id: 'left_forearm_s3', name: 'Numbness', organId: 'left_forearm'), + SymptomModel(id: 'left_forearm_s4', name: 'Tingling', organId: 'left_forearm'), + SymptomModel(id: 'left_forearm_s5', name: 'Swelling', organId: 'left_forearm'), + ], + 'left_wrist': [ + SymptomModel(id: 'left_wrist_s1', name: 'Wrist pain', organId: 'left_wrist'), + SymptomModel(id: 'left_wrist_s2', name: 'Stiffness', organId: 'left_wrist'), + SymptomModel(id: 'left_wrist_s3', name: 'Swelling', organId: 'left_wrist'), + SymptomModel(id: 'left_wrist_s4', name: 'Limited mobility', organId: 'left_wrist'), + SymptomModel(id: 'left_wrist_s5', name: 'Tingling', organId: 'left_wrist'), + SymptomModel(id: 'left_wrist_s6', name: 'Numbness', organId: 'left_wrist'), + ], + + // ARMS - RIGHT + 'right_elbow': [ + SymptomModel(id: 'right_elbow_s1', name: 'Elbow pain', organId: 'right_elbow'), + SymptomModel(id: 'right_elbow_s2', name: 'Stiffness', organId: 'right_elbow'), + SymptomModel(id: 'right_elbow_s3', name: 'Swelling', organId: 'right_elbow'), + SymptomModel(id: 'right_elbow_s4', name: 'Limited mobility', organId: 'right_elbow'), + SymptomModel(id: 'right_elbow_s5', name: 'Tingling', organId: 'right_elbow'), + ], + 'right_forearm': [ + SymptomModel(id: 'right_forearm_s1', name: 'Forearm pain', organId: 'right_forearm'), + SymptomModel(id: 'right_forearm_s2', name: 'Weakness', organId: 'right_forearm'), + SymptomModel(id: 'right_forearm_s3', name: 'Numbness', organId: 'right_forearm'), + SymptomModel(id: 'right_forearm_s4', name: 'Tingling', organId: 'right_forearm'), + SymptomModel(id: 'right_forearm_s5', name: 'Swelling', organId: 'right_forearm'), + ], + 'right_wrist': [ + SymptomModel(id: 'right_wrist_s1', name: 'Wrist pain', organId: 'right_wrist'), + SymptomModel(id: 'right_wrist_s2', name: 'Stiffness', organId: 'right_wrist'), + SymptomModel(id: 'right_wrist_s3', name: 'Swelling', organId: 'right_wrist'), + SymptomModel(id: 'right_wrist_s4', name: 'Limited mobility', organId: 'right_wrist'), + SymptomModel(id: 'right_wrist_s5', name: 'Tingling', organId: 'right_wrist'), + SymptomModel(id: 'right_wrist_s6', name: 'Numbness', organId: 'right_wrist'), + ], + + // LEGS - LEFT + 'left_thigh': [ + SymptomModel(id: 'left_thigh_s1', name: 'Thigh pain', organId: 'left_thigh'), + SymptomModel(id: 'left_thigh_s2', name: 'Muscle cramps', organId: 'left_thigh'), + SymptomModel(id: 'left_thigh_s3', name: 'Weakness', organId: 'left_thigh'), + SymptomModel(id: 'left_thigh_s4', name: 'Numbness', organId: 'left_thigh'), + SymptomModel(id: 'left_thigh_s5', name: 'Swelling', organId: 'left_thigh'), + ], + 'left_knee': [ + SymptomModel(id: 'left_knee_s1', name: 'Knee pain', organId: 'left_knee'), + SymptomModel(id: 'left_knee_s2', name: 'Stiffness', organId: 'left_knee'), + SymptomModel(id: 'left_knee_s3', name: 'Swelling', organId: 'left_knee'), + SymptomModel(id: 'left_knee_s4', name: 'Limited mobility', organId: 'left_knee'), + SymptomModel(id: 'left_knee_s5', name: 'Clicking sound', organId: 'left_knee'), + SymptomModel(id: 'left_knee_s6', name: 'Instability', organId: 'left_knee'), + ], + 'left_shin': [ + SymptomModel(id: 'left_shin_s1', name: 'Shin pain', organId: 'left_shin'), + SymptomModel(id: 'left_shin_s2', name: 'Tenderness', organId: 'left_shin'), + SymptomModel(id: 'left_shin_s3', name: 'Swelling', organId: 'left_shin'), + SymptomModel(id: 'left_shin_s4', name: 'Numbness', organId: 'left_shin'), + ], + 'left_ankle': [ + SymptomModel(id: 'left_ankle_s1', name: 'Ankle pain', organId: 'left_ankle'), + SymptomModel(id: 'left_ankle_s2', name: 'Stiffness', organId: 'left_ankle'), + SymptomModel(id: 'left_ankle_s3', name: 'Swelling', organId: 'left_ankle'), + SymptomModel(id: 'left_ankle_s4', name: 'Limited mobility', organId: 'left_ankle'), + SymptomModel(id: 'left_ankle_s5', name: 'Instability', organId: 'left_ankle'), + ], + + // LEGS - RIGHT + 'right_thigh': [ + SymptomModel(id: 'right_thigh_s1', name: 'Thigh pain', organId: 'right_thigh'), + SymptomModel(id: 'right_thigh_s2', name: 'Muscle cramps', organId: 'right_thigh'), + SymptomModel(id: 'right_thigh_s3', name: 'Weakness', organId: 'right_thigh'), + SymptomModel(id: 'right_thigh_s4', name: 'Numbness', organId: 'right_thigh'), + SymptomModel(id: 'right_thigh_s5', name: 'Swelling', organId: 'right_thigh'), + ], + 'right_knee': [ + SymptomModel(id: 'right_knee_s1', name: 'Knee pain', organId: 'right_knee'), + SymptomModel(id: 'right_knee_s2', name: 'Stiffness', organId: 'right_knee'), + SymptomModel(id: 'right_knee_s3', name: 'Swelling', organId: 'right_knee'), + SymptomModel(id: 'right_knee_s4', name: 'Limited mobility', organId: 'right_knee'), + SymptomModel(id: 'right_knee_s5', name: 'Clicking sound', organId: 'right_knee'), + SymptomModel(id: 'right_knee_s6', name: 'Instability', organId: 'right_knee'), + ], + 'right_shin': [ + SymptomModel(id: 'right_shin_s1', name: 'Shin pain', organId: 'right_shin'), + SymptomModel(id: 'right_shin_s2', name: 'Tenderness', organId: 'right_shin'), + SymptomModel(id: 'right_shin_s3', name: 'Swelling', organId: 'right_shin'), + SymptomModel(id: 'right_shin_s4', name: 'Numbness', organId: 'right_shin'), + ], + 'right_ankle': [ + SymptomModel(id: 'right_ankle_s1', name: 'Ankle pain', organId: 'right_ankle'), + SymptomModel(id: 'right_ankle_s2', name: 'Stiffness', organId: 'right_ankle'), + SymptomModel(id: 'right_ankle_s3', name: 'Swelling', organId: 'right_ankle'), + SymptomModel(id: 'right_ankle_s4', name: 'Limited mobility', organId: 'right_ankle'), + SymptomModel(id: 'right_ankle_s5', name: 'Instability', organId: 'right_ankle'), + ], + + // BACK VIEW ORGANS + 'back_head': [ + SymptomModel(id: 'back_head_s1', name: 'Back of head pain', organId: 'back_head'), + SymptomModel(id: 'back_head_s2', name: 'Neck stiffness', organId: 'back_head'), + SymptomModel(id: 'back_head_s3', name: 'Tension headache', organId: 'back_head'), + ], + 'neck': [ + SymptomModel(id: 'neck_s1', name: 'Neck pain', organId: 'neck'), + SymptomModel(id: 'neck_s2', name: 'Stiffness', organId: 'neck'), + SymptomModel(id: 'neck_s3', name: 'Limited mobility', organId: 'neck'), + SymptomModel(id: 'neck_s4', name: 'Muscle spasm', organId: 'neck'), + SymptomModel(id: 'neck_s5', name: 'Tingling', organId: 'neck'), + ], + 'upper_back': [ + SymptomModel(id: 'upper_back_s1', name: 'Upper back pain', organId: 'upper_back'), + SymptomModel(id: 'upper_back_s2', name: 'Stiffness', organId: 'upper_back'), + SymptomModel(id: 'upper_back_s3', name: 'Muscle tension', organId: 'upper_back'), + SymptomModel(id: 'upper_back_s4', name: 'Sharp pain', organId: 'upper_back'), + SymptomModel(id: 'upper_back_s5', name: 'Difficulty breathing', organId: 'upper_back'), + ], + 'mid_back': [ + SymptomModel(id: 'mid_back_s1', name: 'Mid back pain', organId: 'mid_back'), + SymptomModel(id: 'mid_back_s2', name: 'Stiffness', organId: 'mid_back'), + SymptomModel(id: 'mid_back_s3', name: 'Muscle spasm', organId: 'mid_back'), + SymptomModel(id: 'mid_back_s4', name: 'Tenderness', organId: 'mid_back'), + ], + 'lower_back': [ + SymptomModel(id: 'lower_back_s1', name: 'Lower back pain', organId: 'lower_back'), + SymptomModel(id: 'lower_back_s2', name: 'Stiffness', organId: 'lower_back'), + SymptomModel(id: 'lower_back_s3', name: 'Sharp pain', organId: 'lower_back'), + SymptomModel(id: 'lower_back_s4', name: 'Sciatica', organId: 'lower_back'), + SymptomModel(id: 'lower_back_s5', name: 'Limited mobility', organId: 'lower_back'), + SymptomModel(id: 'lower_back_s6', name: 'Muscle spasm', organId: 'lower_back'), + ], + }; + + /// Get symptoms for a specific organ ID + static List getSymptomsForOrgan(String organId) { + return organSymptomsMap[organId] ?? []; + } + + /// Get symptoms for multiple organ IDs + static Map> getSymptomsForOrgans(List organIds) { + Map> result = {}; + for (String organId in organIds) { + List symptoms = getSymptomsForOrgan(organId); + if (symptoms.isNotEmpty) { + result[organId] = symptoms; + } + } + return result; + } + + /// Check if an organ has symptoms mapped + static bool hasSymptoms(String organId) { + return organSymptomsMap.containsKey(organId) && organSymptomsMap[organId]!.isNotEmpty; + } +} diff --git a/lib/features/symptoms_checker/data/triage_questions_data.dart b/lib/features/symptoms_checker/data/triage_questions_data.dart new file mode 100644 index 0000000..ffaef92 --- /dev/null +++ b/lib/features/symptoms_checker/data/triage_questions_data.dart @@ -0,0 +1,209 @@ +import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; + +class TriageQuestionsData { + static List getSampleTriageQuestions() { + return [ + // Question 1: Pain Location + TriageQuestionModel( + id: 'q1', + question: 'Where do you feel the chest pain, and where does the pain spread or move to?', + options: [ + TriageOptionModel( + id: 'q1_opt1', + text: 'It is all over', + severityScore: 3, + ), + TriageOptionModel( + id: 'q1_opt2', + text: 'It is behind the breast bone', + severityScore: 5, + ), + TriageOptionModel( + id: 'q1_opt3', + text: 'Moves or spread to the upper limb; for example the shoulder, arm, or fingers', + severityScore: 7, + ), + TriageOptionModel( + id: 'q1_opt4', + text: 'Moves or spread to the upper back; between the shoulder blades', + severityScore: 6, + ), + TriageOptionModel( + id: 'q1_opt5', + text: 'Moves or spread to the neck or jaw', + severityScore: 8, + ), + ], + ), + + // Question 2: Pain Intensity + TriageQuestionModel( + id: 'q2', + question: 'How would you describe the intensity of your chest pain?', + options: [ + TriageOptionModel( + id: 'q2_opt1', + text: 'Mild - Barely noticeable, does not interfere with daily activities', + severityScore: 2, + ), + TriageOptionModel( + id: 'q2_opt2', + text: 'Moderate - Noticeable but manageable, some interference with activities', + severityScore: 4, + ), + TriageOptionModel( + id: 'q2_opt3', + text: 'Severe - Significantly interferes with activities, difficult to ignore', + severityScore: 7, + ), + TriageOptionModel( + id: 'q2_opt4', + text: 'Very severe - Unbearable, cannot perform any activities', + severityScore: 9, + ), + ], + ), + + // Question 3: Pain Duration + TriageQuestionModel( + id: 'q3', + question: 'How long have you been experiencing this chest pain?', + options: [ + TriageOptionModel( + id: 'q3_opt1', + text: 'Less than 5 minutes', + severityScore: 3, + ), + TriageOptionModel( + id: 'q3_opt2', + text: 'Between 5 to 15 minutes', + severityScore: 5, + ), + TriageOptionModel( + id: 'q3_opt3', + text: 'Between 15 to 30 minutes', + severityScore: 7, + ), + TriageOptionModel( + id: 'q3_opt4', + text: 'More than 30 minutes', + severityScore: 8, + ), + TriageOptionModel( + id: 'q3_opt5', + text: 'Comes and goes (intermittent)', + severityScore: 4, + ), + ], + ), + + // Question 4: Associated Symptoms + TriageQuestionModel( + id: 'q4', + question: 'Are you experiencing any of these symptoms along with chest pain?', + options: [ + TriageOptionModel( + id: 'q4_opt1', + text: 'Shortness of breath or difficulty breathing', + severityScore: 8, + ), + TriageOptionModel( + id: 'q4_opt2', + text: 'Sweating, nausea, or vomiting', + severityScore: 7, + ), + TriageOptionModel( + id: 'q4_opt3', + text: 'Dizziness or lightheadedness', + severityScore: 7, + ), + TriageOptionModel( + id: 'q4_opt4', + text: 'Rapid or irregular heartbeat', + severityScore: 6, + ), + TriageOptionModel( + id: 'q4_opt5', + text: 'None of the above', + severityScore: 2, + ), + ], + ), + + // Question 5: Triggering Factors + TriageQuestionModel( + id: 'q5', + question: 'What triggers or worsens your chest pain?', + options: [ + TriageOptionModel( + id: 'q5_opt1', + text: 'Physical activity or exertion', + severityScore: 6, + ), + TriageOptionModel( + id: 'q5_opt2', + text: 'Emotional stress or anxiety', + severityScore: 4, + ), + TriageOptionModel( + id: 'q5_opt3', + text: 'Deep breathing or coughing', + severityScore: 3, + ), + TriageOptionModel( + id: 'q5_opt4', + text: 'Eating or lying down', + severityScore: 3, + ), + TriageOptionModel( + id: 'q5_opt5', + text: 'Nothing specific, pain is constant', + severityScore: 7, + ), + ], + ), + ]; + } + + /// Calculate total severity score from answered questions + static int calculateTotalScore(List questions) { + int totalScore = 0; + int answeredCount = 0; + + for (var question in questions) { + if (question.isConfirmed && question.confirmedOption != null) { + totalScore += question.confirmedOption!.severityScore ?? 0; + answeredCount++; + } + } + + // Return average score or 0 if no questions answered + return answeredCount > 0 ? (totalScore / answeredCount * 10).round() : 0; + } + + /// Get risk level based on score + static String getRiskLevel(int score) { + if (score >= 70) { + return 'High Risk - Seek immediate medical attention'; + } else if (score >= 50) { + return 'Moderate Risk - Consult a doctor soon'; + } else if (score >= 30) { + return 'Low to Moderate Risk - Monitor symptoms'; + } else { + return 'Low Risk - Self-care may be sufficient'; + } + } + + /// Get suggested condition based on score + static String getSuggestedCondition(int score) { + if (score >= 70) { + return 'Acute Coronary Syndrome'; + } else if (score >= 50) { + return 'Angina or Cardiac concern'; + } else if (score >= 30) { + return 'Non-cardiac chest pain'; + } else { + return 'Musculoskeletal chest pain'; + } + } +} diff --git a/lib/features/symptoms_checker/models/conditions_model.dart b/lib/features/symptoms_checker/models/conditions_model.dart new file mode 100644 index 0000000..774d8c3 --- /dev/null +++ b/lib/features/symptoms_checker/models/conditions_model.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; + +class ConditionsModel { + final IconData icon; + final String title; + final int percentage; + final String tagText; + final String clinic; + final List symptoms; + final String description; + final String? monitorNote; + final String? appointmentLabel; + final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum; + + ConditionsModel({ + required this.icon, + required this.title, + required this.percentage, + required this.tagText, + required this.clinic, + required this.symptoms, + required this.description, + required this.possibleConditionsSeverityEnum, + this.monitorNote, + this.appointmentLabel, + }); +} + +List dummyConditions = [ + ConditionsModel( + icon: Icons.psychology_alt, + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice, + title: "Migraine", + percentage: 87, + tagText: "Seek Medical Advice", + clinic: "Internal Medicine Clinic", + symptoms: ["Headache", "Nausea", "Sensitivity to light"], + description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.", + appointmentLabel: "Book Appointment", + ), + ConditionsModel( + icon: Icons.deblur, + title: "Tension Headache", + percentage: 37, + tagText: "Monitor", + monitorNote: "No need to visit doctor", + clinic: "GP Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly, + ), + ConditionsModel( + icon: Icons.medication_liquid, + title: "Meningitis", + percentage: 28, + tagText: "Emergency", + clinic: "Neurology Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency, + ), + ConditionsModel( + icon: Icons.psychology_alt, + title: "Migraine", + percentage: 87, + tagText: "Seek Medical Advice", + clinic: "Internal Medicine Clinic", + symptoms: ["Headache", "Nausea", "Sensitivity to light"], + description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice, + ), + ConditionsModel( + icon: Icons.deblur, + title: "Tension Headache", + percentage: 37, + tagText: "Monitor", + monitorNote: "No need to visit doctor", + clinic: "GP Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly, + ), + ConditionsModel( + icon: Icons.medication_liquid, + title: "Meningitis", + percentage: 28, + tagText: "Emergency", + clinic: "Neurology Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency, + ), +]; diff --git a/lib/features/symptoms_checker/models/organ_model.dart b/lib/features/symptoms_checker/models/organ_model.dart new file mode 100644 index 0000000..fab7dff --- /dev/null +++ b/lib/features/symptoms_checker/models/organ_model.dart @@ -0,0 +1,43 @@ +import 'package:hmg_patient_app_new/core/enums.dart'; + +class OrganModel { + final String id; + final String name; + final String description; + final BodyView bodyView; + final OrganPosition position; + + const OrganModel({ + required this.id, + required this.name, + required this.description, + required this.bodyView, + required this.position, + }); + + OrganModel copyWith({ + String? id, + String? name, + String? description, + BodyView? bodyView, + OrganPosition? position, + }) { + return OrganModel( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + bodyView: bodyView ?? this.bodyView, + position: position ?? this.position, + ); + } +} + +class OrganPosition { + final double x; // Percentage from left (0.0 to 1.0) + final double y; // Percentage from top (0.0 to 1.0) + + const OrganPosition({ + required this.x, + required this.y, + }); +} diff --git a/lib/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart b/lib/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart new file mode 100644 index 0000000..7c8f915 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart @@ -0,0 +1,151 @@ +class BodySymptomResponseModel { + final DataDetails? dataDetails; + + BodySymptomResponseModel({ + this.dataDetails, + }); + + factory BodySymptomResponseModel.fromJson(Map json) { + return BodySymptomResponseModel( + dataDetails: json['dataDetails'] != null ? DataDetails.fromJson(json['dataDetails']) : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.toJson(), + }; + } +} + +class DataDetails { + final List? result; + final int? id; + final dynamic exception; + final int? status; + final bool? isCanceled; + final bool? isCompleted; + final bool? isCompletedSuccessfully; + final int? creationOptions; + final dynamic asyncState; + final bool? isFaulted; + + DataDetails({ + this.result, + this.id, + this.exception, + this.status, + this.isCanceled, + this.isCompleted, + this.isCompletedSuccessfully, + this.creationOptions, + this.asyncState, + this.isFaulted, + }); + + factory DataDetails.fromJson(Map json) { + return DataDetails( + result: json['Result'] != null ? (json['Result'] as List).map((item) => OrganSymptomResult.fromJson(item)).toList() : null, + id: json['Id'], + exception: json['Exception'], + status: json['Status'], + isCanceled: json['IsCanceled'], + isCompleted: json['IsCompleted'], + isCompletedSuccessfully: json['IsCompletedSuccessfully'], + creationOptions: json['CreationOptions'], + asyncState: json['AsyncState'], + isFaulted: json['IsFaulted'], + ); + } + + Map toJson() { + return { + 'Result': result?.map((item) => item.toJson()).toList(), + 'Id': id, + 'Exception': exception, + 'Status': status, + 'IsCanceled': isCanceled, + 'IsCompleted': isCompleted, + 'IsCompletedSuccessfully': isCompletedSuccessfully, + 'CreationOptions': creationOptions, + 'AsyncState': asyncState, + 'IsFaulted': isFaulted, + }; + } +} + +class OrganSymptomResult { + final String? name; + final List? bodySymptoms; + + OrganSymptomResult({ + this.name, + this.bodySymptoms, + }); + + factory OrganSymptomResult.fromJson(Map json) { + return OrganSymptomResult( + name: json['name'], + bodySymptoms: json['bodySymptoms'] != null ? (json['bodySymptoms'] as List).map((item) => BodySymptom.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'name': name, + 'bodySymptoms': bodySymptoms?.map((item) => item.toJson()).toList(), + }; + } +} + +class BodySymptom { + final String? id; + final String? type; + final String? symptomsName; + final String? name; + final String? commonName; + final String? nameAr; + final String? commonNameAr; + + BodySymptom({ + this.id, + this.type, + this.symptomsName, + this.name, + this.commonName, + this.nameAr, + this.commonNameAr, + }); + + factory BodySymptom.fromJson(Map json) { + return BodySymptom( + id: json['Id'], + type: json['type'], + symptomsName: json['symptoms_name'], + name: json['name'], + commonName: json['common_name'], + nameAr: json['nameAr'], + commonNameAr: json['common_nameAr'], + ); + } + + Map toJson() { + return { + 'Id': id, + 'type': type, + 'symptoms_name': symptomsName, + 'name': name, + 'common_name': commonName, + 'nameAr': nameAr, + 'common_nameAr': commonNameAr, + }; + } + + // Helper method to get display name based on locale + String getDisplayName(bool isArabic) { + if (isArabic) { + return commonNameAr ?? nameAr ?? commonName ?? name ?? ''; + } + return commonName ?? name ?? ''; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart b/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart new file mode 100644 index 0000000..b2be4a2 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart @@ -0,0 +1,59 @@ +class RiskAndSuggestionsResponseModel { + final List? dataDetails; + + RiskAndSuggestionsResponseModel({this.dataDetails}); + + factory RiskAndSuggestionsResponseModel.fromJson(Map json) { + return RiskAndSuggestionsResponseModel( + dataDetails: + json['dataDetails'] != null ? (json['dataDetails'] as List).map((item) => RiskAndSuggestionsItemModel.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.map((item) => item.toJson()).toList(), + }; + } +} + +class RiskAndSuggestionsItemModel { + final String? id; + final String? type; + final String? name; + final String? commonName; + final String? language; + + RiskAndSuggestionsItemModel({ + this.id, + this.type, + this.name, + this.commonName, + this.language, + }); + + factory RiskAndSuggestionsItemModel.fromJson(Map json) { + return RiskAndSuggestionsItemModel( + id: json['id'], + type: json['type'], + name: json['name'], + commonName: json['common_name'], + language: json['language'], + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + 'name': name, + 'common_name': commonName, + 'language': language, + }; + } + + // Helper method to get display name + String getDisplayName() { + return commonName ?? name ?? ''; + } +} diff --git a/lib/features/symptoms_checker/models/symptom_model.dart b/lib/features/symptoms_checker/models/symptom_model.dart new file mode 100644 index 0000000..19d8be4 --- /dev/null +++ b/lib/features/symptoms_checker/models/symptom_model.dart @@ -0,0 +1,105 @@ +class SymptomModel { + final String id; + final String name; + final String nameAr; + final String? organId; + + const SymptomModel({ + required this.id, + required this.name, + this.nameAr = '', + this.organId, + }); + + SymptomModel copyWith({ + String? id, + String? name, + String? nameAr, + String? organId, + }) { + return SymptomModel( + id: id ?? this.id, + name: name ?? this.name, + nameAr: nameAr ?? this.nameAr, + organId: organId ?? this.organId, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'nameAr': nameAr, + 'organId': organId, + }; + } + + factory SymptomModel.fromJson(Map json) { + return SymptomModel( + id: json['id'] ?? '', + name: json['name'] ?? '', + nameAr: json['nameAr'] ?? '', + organId: json['organId'], + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SymptomModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return 'SymptomModel(id: $id, name: $name, nameAr: $nameAr, organId: $organId)'; + } +} + +/// Class to group symptoms by organ +class OrganSymptomsGroup { + final String organId; + final String organName; + final List symptoms; + final Set selectedSymptomIds; + + OrganSymptomsGroup({ + required this.organId, + required this.organName, + required this.symptoms, + Set? selectedSymptomIds, + }) : selectedSymptomIds = selectedSymptomIds ?? {}; + + /// Get count of selected symptoms + int get selectedCount => selectedSymptomIds.length; + + /// Check if any symptoms are selected + bool get hasSelectedSymptoms => selectedSymptomIds.isNotEmpty; + + /// Get list of selected symptoms + List get selectedSymptoms { + return symptoms.where((symptom) => selectedSymptomIds.contains(symptom.id)).toList(); + } + + OrganSymptomsGroup copyWith({ + String? organId, + String? organName, + List? symptoms, + Set? selectedSymptomIds, + }) { + return OrganSymptomsGroup( + organId: organId ?? this.organId, + organName: organName ?? this.organName, + symptoms: symptoms ?? this.symptoms, + selectedSymptomIds: selectedSymptomIds ?? this.selectedSymptomIds, + ); + } + + @override + String toString() { + return 'OrganSymptomsGroup(organId: $organId, organName: $organName, symptomsCount: ${symptoms.length}, selectedCount: $selectedCount)'; + } +} + diff --git a/lib/features/symptoms_checker/models/triage_question_model.dart b/lib/features/symptoms_checker/models/triage_question_model.dart new file mode 100644 index 0000000..fb8ab20 --- /dev/null +++ b/lib/features/symptoms_checker/models/triage_question_model.dart @@ -0,0 +1,85 @@ +class TriageQuestionModel { + final String id; + final String question; + final List options; + int? selectedOptionIndex; + int? confirmedOptionIndex; // Confirmed answer when user presses Next + + TriageQuestionModel({ + required this.id, + required this.question, + required this.options, + this.selectedOptionIndex, + this.confirmedOptionIndex, + }); + + bool get isAnswered => selectedOptionIndex != null; + + bool get isConfirmed => confirmedOptionIndex != null; + + void selectOption(int index) { + selectedOptionIndex = index; + } + + void confirmSelection() { + confirmedOptionIndex = selectedOptionIndex; + } + + void clearSelection() { + selectedOptionIndex = null; + } + + TriageOptionModel? get selectedOption { + if (selectedOptionIndex != null && selectedOptionIndex! < options.length) { + return options[selectedOptionIndex!]; + } + return null; + } + + TriageOptionModel? get confirmedOption { + if (confirmedOptionIndex != null && confirmedOptionIndex! < options.length) { + return options[confirmedOptionIndex!]; + } + return null; + } + + TriageQuestionModel copyWith({ + String? id, + String? question, + List? options, + int? selectedOptionIndex, + int? confirmedOptionIndex, + }) { + return TriageQuestionModel( + id: id ?? this.id, + question: question ?? this.question, + options: options ?? this.options, + selectedOptionIndex: selectedOptionIndex ?? this.selectedOptionIndex, + confirmedOptionIndex: confirmedOptionIndex ?? this.confirmedOptionIndex, + ); + } +} + +class TriageOptionModel { + final String id; + final String text; + final int? severityScore; // Optional: for calculating risk scores + + TriageOptionModel({ + required this.id, + required this.text, + this.severityScore, + }); + + TriageOptionModel copyWith({ + String? id, + String? text, + int? severityScore, + }) { + return TriageOptionModel( + id: id ?? this.id, + text: text ?? this.text, + severityScore: severityScore ?? this.severityScore, + ); + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart new file mode 100644 index 0000000..954d414 --- /dev/null +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -0,0 +1,223 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class SymptomsCheckerRepo { + Future>> getBodySymptomsByName({ + required List organNames, + }); + + Future>> getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }); + + Future>> getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }); +} + +class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService}); + + @override + Future>> getBodySymptomsByName({required List organNames}) async { + log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); + log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}"); + + Map headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBodySymptomsByName, + apiHeaders: headers, + body: jsonEncode(organNames), + isExternal: true, + isAllowAny: true, + isBodyPlainText: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("GetBodySymptomsByName API Failed: $error"); + log("GetBodySymptomsByName Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("GetBodySymptomsByName Response Status: $statusCode"); + loggerService.logInfo("GetBodySymptomsByName API Success: $response"); + log("GetBodySymptomsByName Response: $response"); + + BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: bodySymptomResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Parse Error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getBodySymptomsByName: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRiskFactors, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + log("GetRiskFactors Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("GetRiskFactors Response: $response"); + + // Parse response if it's a string + final Map responseData = response is String ? jsonDecode(response) : response; + + RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing GetRiskFactors response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Parse Error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getRiskFactors: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getSuggestions, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + log("getSuggestions Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("getSuggestions Response: $response"); + + // Parse response if it's a string + final Map responseData = response is String ? jsonDecode(response) : response; + + RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing getSuggestions response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Parse Error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getSuggestions: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart new file mode 100644 index 0000000..da439c6 --- /dev/null +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -0,0 +1,772 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class SymptomsCheckerViewModel extends ChangeNotifier { + final SymptomsCheckerRepo symptomsCheckerRepo; + final ErrorHandlerService errorHandlerService; + final AppState appState; + + SymptomsCheckerViewModel({ + required this.symptomsCheckerRepo, + required this.errorHandlerService, + required this.appState, + }); + + // State variables + bool isBodyHidden = false; + BodyView _currentView = BodyView.front; + final Set _selectedOrganIds = {}; + bool _isBottomSheetExpanded = false; + + // Tooltip state + String? _tooltipOrganId; + Timer? _tooltipTimer; + + // API loading states + bool isBodySymptomsLoading = false; + bool isRiskFactorsLoading = false; + bool isSuggestionsLoading = false; + + // API data storage - using API models directly + BodySymptomResponseModel? bodySymptomResponse; + RiskAndSuggestionsResponseModel? riskFactorsResponse; + RiskAndSuggestionsResponseModel? suggestionsResponse; + + // Selected risk factors tracking + final Set _selectedRiskFactorIds = {}; + + // Selected Suggestions tracking + final Set _selectedSuggestionsIds = {}; + + // Selected symptoms tracking (organId -> Set of symptom IDs) + final Map> _selectedSymptomsByOrgan = {}; + + // User Info Flow State + int _userInfoCurrentPage = 0; + bool _isSinglePageEditMode = false; // Track if editing single page or full flow + String? _selectedGender; + DateTime? _dateOfBirth; + int? _selectedAge; + double _selectedHeight = 170; + bool _isHeightCm = true; + double _selectedWeight = 60; + bool _isWeightKg = true; + + // Getters + + bool isPossibleConditionsLoading = false; + + // User Info Getters + int get userInfoCurrentPage => _userInfoCurrentPage; + + bool get isSinglePageEditMode => _isSinglePageEditMode; + + String? get selectedGender => _selectedGender; + + DateTime? get dateOfBirth => _dateOfBirth; + + int? get selectedAge => _selectedAge; + + double? get selectedHeight => _selectedHeight; + + bool get isHeightCm => _isHeightCm; + + double? get selectedWeight => _selectedWeight; + + bool get isWeightKg => _isWeightKg; + + BodyView get currentView => _currentView; + + Set get selectedOrganIds => _selectedOrganIds; + + bool get isBottomSheetExpanded => _isBottomSheetExpanded; + + String? get tooltipOrganId => _tooltipOrganId; + + /// Get organs for current view + List get currentOrgans => OrganData.getOrgansForView(_currentView); + + /// Get all selected organs from both views + List get selectedOrgans { + final allOrgans = [ + ...OrganData.frontViewOrgans, + ...OrganData.backViewOrgans, + ]; + return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList(); + } + + /// Check if any organs are selected + bool get hasSelectedOrgans => _selectedOrganIds.isNotEmpty; + + /// Get count of selected organs + int get selectedOrgansCount => _selectedOrganIds.length; + + List get organSymptomsResults { + if (bodySymptomResponse?.dataDetails?.result == null) { + return []; + } + return bodySymptomResponse!.dataDetails!.result ?? []; + } + + int get totalSelectedSymptomsCount { + return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); + } + + bool get hasSelectedSymptoms { + return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); + } + + /// Get risk factors list + List get riskFactorsList { + return riskFactorsResponse?.dataDetails ?? []; + } + + /// Check if any risk factors are selected + bool get hasSelectedRiskFactors => _selectedRiskFactorIds.isNotEmpty; + + /// Get selected risk factors count + int get selectedRiskFactorsCount => _selectedRiskFactorIds.length; + + /// Check if any risk factors are selected + bool get hasSelectedSuggestions => _selectedSuggestionsIds.isNotEmpty; + + /// Get selected risk factors count + int get selectedSuggestionsCount => _selectedSuggestionsIds.length; + + /// Get risk factors list + List get suggestionsList { + return suggestionsResponse?.dataDetails ?? []; + } + + void toggleView() { + _currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; + notifyListeners(); + } + + toggleIsBodyHidden() { + isBodyHidden = !isBodyHidden; + notifyListeners(); + } + + void toggleOrganSelection(String organId) { + if (selectedOrganIds.isEmpty && _isBottomSheetExpanded == false) { + toggleBottomSheet(); + } + + if (_selectedOrganIds.contains(organId)) { + _selectedOrganIds.remove(organId); + } else { + _selectedOrganIds.add(organId); + } + + // Show tooltip + _showTooltip(organId); + + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } + + notifyListeners(); + } + + void _showTooltip(String organId) { + _tooltipTimer?.cancel(); + + _tooltipOrganId = organId; + notifyListeners(); + + // Hide tooltip after 2 seconds + _tooltipTimer = Timer(const Duration(seconds: 1), () { + _tooltipOrganId = null; + notifyListeners(); + }); + } + + /// Hide tooltip immediately + void hideTooltip() { + _tooltipTimer?.cancel(); + _tooltipOrganId = null; + notifyListeners(); + } + + void removeOrgan(String organId) { + _selectedOrganIds.remove(organId); + notifyListeners(); + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } + } + + void clearAllSelections() { + _selectedOrganIds.clear(); + notifyListeners(); + } + + void toggleBottomSheet({bool? value}) { + if (value != null) { + _isBottomSheetExpanded = value; + notifyListeners(); + return; + } + _isBottomSheetExpanded = !_isBottomSheetExpanded; + notifyListeners(); + } + + void setBottomSheetExpanded(bool isExpanded) { + _isBottomSheetExpanded = isExpanded; + notifyListeners(); + } + + bool validateSelection() { + return _selectedOrganIds.isNotEmpty; + } + + List getSelectedOrganIds() { + return _selectedOrganIds.toList(); + } + + List getSelectedOrganNames() { + return selectedOrgans.map((organ) => organ.description).toList(); + } + + Future initializeSymptomGroups({ + Function()? onSuccess, + Function(String)? onError, + }) async { + if (_selectedOrganIds.isEmpty) { + if (onError != null) { + onError('No organs selected'); + } + return; + } + + List organNames = selectedOrgans.map((organ) => organ.name).toList(); + + await getBodySymptomsByName( + organNames: organNames, + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + void toggleSymptomSelection(String organId, String symptomId) { + if (!_selectedSymptomsByOrgan.containsKey(organId)) { + _selectedSymptomsByOrgan[organId] = {}; + } + + if (_selectedSymptomsByOrgan[organId]!.contains(symptomId)) { + _selectedSymptomsByOrgan[organId]!.remove(symptomId); + } else { + _selectedSymptomsByOrgan[organId]!.add(symptomId); + } + notifyListeners(); + } + + bool isSymptomSelected(String organId, String symptomId) { + return _selectedSymptomsByOrgan[organId]?.contains(symptomId) ?? false; + } + + List getAllSelectedSymptoms() { + List allSymptoms = []; + + if (bodySymptomResponse?.dataDetails?.result == null) { + return allSymptoms; + } + + for (var organResult in bodySymptomResponse!.dataDetails!.result!) { + String? matchingOrganId; + for (var organ in selectedOrgans) { + if (organ.name == organResult.name) { + matchingOrganId = organ.id; + break; + } + } + + if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { + final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!; + + if (organResult.bodySymptoms != null) { + for (var symptom in organResult.bodySymptoms!) { + if (symptom.id != null && selectedIds.contains(symptom.id)) { + allSymptoms.add(symptom); + } + } + } + } + } + + return allSymptoms; + } + + void clearAllSymptomSelections() { + _selectedSymptomsByOrgan.clear(); + notifyListeners(); + } + + // Risk Factors Methods + + /// Toggle risk factor selection + void toggleRiskFactorSelection(String riskFactorId) { + if (riskFactorId == "not_applicable") { + // "Not applicable" is mutually exclusive: if selected, clear all others + if (_selectedRiskFactorIds.contains(riskFactorId)) { + _selectedRiskFactorIds.remove(riskFactorId); + } else { + _selectedRiskFactorIds + ..clear() + ..add(riskFactorId); + } + } else { + _selectedRiskFactorIds.remove("not_applicable"); + if (!_selectedRiskFactorIds.add(riskFactorId)) { + _selectedRiskFactorIds.remove(riskFactorId); + } + } + + notifyListeners(); + } + + /// Check if risk factor is selected + bool isRiskFactorSelected(String riskFactorId) { + return _selectedRiskFactorIds.contains(riskFactorId); + } + + /// Get all selected risk factors + List getAllSelectedRiskFactors() { + return riskFactorsList.where((factor) => factor.id != null && _selectedRiskFactorIds.contains(factor.id)).toList(); + } + + /// Clear all risk factor selections + void clearAllRiskFactorSelections() { + _selectedRiskFactorIds.clear(); + notifyListeners(); + } + + /// Fetch risk factors based on selected symptoms + Future fetchRiskFactors({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Get all selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + + if (selectedSymptoms.isEmpty) { + if (onError != null) { + onError('No symptoms selected'); + } + return; + } + + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Extract symptom IDs + List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + + await getRiskFactors( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Call Risk Factors API + Future getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + Function(RiskAndSuggestionsResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isRiskFactorsLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getRiskFactors( + age: age, + sex: sex, + evidenceIds: evidenceIds, + language: language, + ); + + result.fold( + (failure) async { + isRiskFactorsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isRiskFactorsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + riskFactorsResponse = apiResponse.data; + + if (riskFactorsResponse != null && riskFactorsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + id: "not_applicable", + commonName: "Not Applicable", + name: "Not Applicable", + language: appState.isArabic() ? 'ar' : 'en', + type: null, + ); + riskFactorsResponse!.dataDetails!.add(riskFactorItem); + } + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors'); + } + } + }, + ); + } + + // Suggestions Methods + + /// Toggle suggestions selection + void toggleSuggestionsSelection(String suggestionsId) { + if (suggestionsId == "not_applicable") { + // "Not applicable" is mutually exclusive: if selected, clear all others + if (_selectedSuggestionsIds.contains(suggestionsId)) { + _selectedSuggestionsIds.remove(suggestionsId); + } else { + _selectedSuggestionsIds + ..clear() + ..add(suggestionsId); + } + } else { + _selectedSuggestionsIds.remove("not_applicable"); + if (!_selectedSuggestionsIds.add(suggestionsId)) { + _selectedSuggestionsIds.remove(suggestionsId); + } + } + + notifyListeners(); + } + + /// Check if risk factor is selected + bool isSuggestionsSelected(String riskFactorId) { + return _selectedSuggestionsIds.contains(riskFactorId); + } + + /// Get all selected risk factors + List getAllSelectedSuggestions() { + return suggestionsList.where((factor) => factor.id != null && _selectedSuggestionsIds.contains(factor.id)).toList(); + } + + /// Clear all risk factor selections + void clearAllSuggestionsSelections() { + _selectedSuggestionsIds.clear(); + notifyListeners(); + } + + /// Fetch risk factors based on selected symptoms + Future fetchSuggestions({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Get all selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + + if (selectedSymptoms.isEmpty) { + if (onError != null) { + onError('No symptoms selected'); + } + return; + } + + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Extract symptom IDs + List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + + // Get all selected symptoms + final selectedRisks = getAllSelectedRiskFactors(); + + if (selectedRisks.isNotEmpty) { + List evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList(); + evidenceIds.addAll(evidenceRisksIds); + } + + await getSuggestions( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Call Suggestions API + Future getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + Function(RiskAndSuggestionsResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isSuggestionsLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getSuggestions( + age: age, + sex: sex, + evidenceIds: evidenceIds, + language: language, + ); + + result.fold( + (failure) async { + isSuggestionsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isSuggestionsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + suggestionsResponse = apiResponse.data; + + if (suggestionsResponse != null && suggestionsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + id: "not_applicable", + commonName: "Not Applicable", + name: "Not Applicable", + language: appState.isArabic() ? 'ar' : 'en', + type: null, + ); + suggestionsResponse!.dataDetails!.add(riskFactorItem); + } + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors'); + } + } + }, + ); + } + + void reset() { + _currentView = BodyView.front; + _selectedOrganIds.clear(); + _selectedSymptomsByOrgan.clear(); + _selectedRiskFactorIds.clear(); + _selectedSuggestionsIds.clear(); + bodySymptomResponse = null; + riskFactorsResponse = null; + suggestionsResponse = null; + _isBottomSheetExpanded = false; + _tooltipTimer?.cancel(); + _tooltipOrganId = null; + // Reset user info flow + _userInfoCurrentPage = 0; + _isSinglePageEditMode = false; + _selectedGender = null; + _dateOfBirth = null; + _selectedAge = null; + _selectedHeight = 170; + _isHeightCm = true; + _selectedWeight = 60; + _isWeightKg = true; + notifyListeners(); + } + + // User Info Flow Methods + + /// Set current page in user info flow + void setUserInfoPage(int page, {bool isSinglePageEdit = false}) { + _userInfoCurrentPage = page; + _isSinglePageEditMode = isSinglePageEdit; + notifyListeners(); + } + + /// Set single page edit mode + void setSinglePageEditMode(bool isSingleEdit) { + _isSinglePageEditMode = isSingleEdit; + notifyListeners(); + } + + /// Navigate to next page in user info flow + void nextUserInfoPage() { + if (_userInfoCurrentPage < 3) { + _userInfoCurrentPage++; + notifyListeners(); + } + } + + /// Navigate to previous page in user info flow + void previousUserInfoPage() { + if (_userInfoCurrentPage > 0) { + _userInfoCurrentPage--; + notifyListeners(); + } + } + + /// Set selected gender + void setGender(String gender) { + _selectedGender = gender; + notifyListeners(); + } + + /// Set selected age + void setAge(int age) { + _selectedAge = age; + notifyListeners(); + } + + /// Set date of birth + void setDateOfBirth(DateTime dateOfBirth) { + _dateOfBirth = dateOfBirth; + // Calculate age from date of birth + final now = DateTime.now(); + int age = now.year - dateOfBirth.year; + if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { + age--; + } + _selectedAge = age; + notifyListeners(); + } + + /// Set selected height + void setHeight(double height, bool isCm) { + _selectedHeight = height; + _isHeightCm = isCm; + notifyListeners(); + } + + /// Set selected weight + void setWeight(double weight, bool isKg) { + _selectedWeight = weight; + _isWeightKg = isKg; + notifyListeners(); + } + + /// Check if user info page is last + bool get isUserInfoLastPage => _userInfoCurrentPage == 3; + + bool get isUserInfoFirstPage => _userInfoCurrentPage == 0; + + /// Validate and submit user info + Map getUserInfoData() { + return { + 'gender': _selectedGender, + 'dateOfBirth': _dateOfBirth?.toIso8601String(), + 'age': _selectedAge, + 'height': _selectedHeight, + 'heightUnit': _isHeightCm ? 'cm' : 'ft', + 'weight': _selectedWeight, + 'weightUnit': _isWeightKg ? 'kg' : 'lbs', + }; + } + + Future getBodySymptomsByName({ + required List organNames, + Function(BodySymptomResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isBodySymptomsLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getBodySymptomsByName( + organNames: organNames, + ); + + result.fold( + (failure) async { + isBodySymptomsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isBodySymptomsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + bodySymptomResponse = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms'); + } + } + }, + ); + } + + @override + void dispose() { + _tooltipTimer?.cancel(); + super.dispose(); + } +} diff --git a/lib/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart b/lib/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart new file mode 100644 index 0000000..c014bee --- /dev/null +++ b/lib/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart @@ -0,0 +1,115 @@ +// Dart model for the "AncillaryOrderList" structure +// Uses DateUtil.convertStringToDate and DateUtil.dateToDotNetString from your project to parse/serialize .NET-style dates. +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class AncillaryOrderListModel { + List? ancillaryOrderList; + + AncillaryOrderListModel({this.ancillaryOrderList}); + + factory AncillaryOrderListModel.fromJson(Map json) => AncillaryOrderListModel( + ancillaryOrderList: json['AncillaryOrderList'] != null + ? List.from( + (json['AncillaryOrderList'] as List).map( + (x) => AncillaryOrderGroup.fromJson(x as Map), + ), + ) + : null, + ); +} + +class AncillaryOrderGroup { + List? ancillaryOrderList; + dynamic errCode; + String? message; + int? patientID; + String? patientName; + int? patientType; + int? projectID; + String? projectName; + String? setupID; + int? statusCode; + + AncillaryOrderGroup({ + this.ancillaryOrderList, + this.errCode, + this.message, + this.patientID, + this.patientName, + this.patientType, + this.projectID, + this.projectName, + this.setupID, + this.statusCode, + }); + + factory AncillaryOrderGroup.fromJson(Map json) => AncillaryOrderGroup( + ancillaryOrderList: json['AncillaryOrderList'] != null + ? List.from( + (json['AncillaryOrderList'] as List).map( + (x) => AncillaryOrderItem.fromJson(x as Map), + ), + ) + : null, + errCode: json['ErrCode'], + message: json['Message'] as String?, + patientID: json['PatientID'] as int?, + patientName: json['PatientName'] as String?, + patientType: json['PatientType'] as int?, + projectID: json['ProjectID'] as int?, + projectName: json['ProjectName'] as String?, + setupID: json['SetupID'] as String?, + statusCode: json['StatusCode'] as int?, + ); +} + +class AncillaryOrderItem { + dynamic ancillaryProcedureListModels; + DateTime? appointmentDate; + int? appointmentNo; + int? clinicID; + String? clinicName; + int? doctorID; + String? doctorName; + int? invoiceNo; + bool? isCheckInAllow; + bool? isQueued; + DateTime? orderDate; + int? orderNo; + String? projectName; // Added from parent AncillaryOrderGroup + int? projectID; // Added from parent AncillaryOrderGroup + + AncillaryOrderItem({ + this.ancillaryProcedureListModels, + this.appointmentDate, + this.appointmentNo, + this.clinicID, + this.clinicName, + this.doctorID, + this.doctorName, + this.invoiceNo, + this.isCheckInAllow, + this.isQueued, + this.orderDate, + this.orderNo, + this.projectName, + this.projectID, + }); + + factory AncillaryOrderItem.fromJson(Map json, {String? projectName, int? projectID}) => AncillaryOrderItem( + ancillaryProcedureListModels: json['AncillaryProcedureListModels'], + appointmentDate: DateUtil.convertStringToDate(json['AppointmentDate']), + appointmentNo: json['AppointmentNo'] as int?, + clinicID: json['ClinicID'] as int?, + clinicName: json['ClinicName'] as String?, + doctorID: json['DoctorID'] as int?, + doctorName: json['DoctorName'] as String?, + invoiceNo: json['Invoiceno'] as int?, + isCheckInAllow: json['IsCheckInAllow'] as bool?, + isQueued: json['IsQueued'] as bool?, + orderDate: DateUtil.convertStringToDate(json['OrderDate']), + orderNo: json['OrderNo'] as int?, + projectName: projectName, + projectID: projectID, + ); +} diff --git a/lib/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart b/lib/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart new file mode 100644 index 0000000..26abde3 --- /dev/null +++ b/lib/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart @@ -0,0 +1,221 @@ +// Dart model classes for "AncillaryOrderProcList" +// Generated for user: faizatflutter +// Uses DateUtil.convertStringToDate for parsing .NET-style dates (same approach as your PatientRadiologyResponseModel) + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class AncillaryOrderProcListModel { + List? ancillaryOrderProcList; + + AncillaryOrderProcListModel({this.ancillaryOrderProcList}); + + factory AncillaryOrderProcListModel.fromJson(Map json) => AncillaryOrderProcListModel( + ancillaryOrderProcList: json['AncillaryOrderProcList'] != null + ? List.from( + (json['AncillaryOrderProcList'] as List).map( + (x) => AncillaryOrderProcedureItem.fromJson(x as Map), + ), + ) + : null, + ); +} + +class AncillaryOrderProcedureItem { + List? ancillaryOrderProcDetailsList; + DateTime? appointmentDate; + int? appointmentNo; + int? clinicID; + String? clinicName; + int? companyID; + String? companyName; + int? doctorID; + String? doctorName; + dynamic errCode; + int? groupID; + String? insurancePolicyNo; + String? message; + String? patientCardID; + int? patientID; + String? patientName; + int? patientType; + int? policyID; + String? policyName; + int? projectID; + String? setupID; + int? statusCode; + int? subCategoryID; + String? subPolicyNo; + + AncillaryOrderProcedureItem({ + this.ancillaryOrderProcDetailsList, + this.appointmentDate, + this.appointmentNo, + this.clinicID, + this.clinicName, + this.companyID, + this.companyName, + this.doctorID, + this.doctorName, + this.errCode, + this.groupID, + this.insurancePolicyNo, + this.message, + this.patientCardID, + this.patientID, + this.patientName, + this.patientType, + this.policyID, + this.policyName, + this.projectID, + this.setupID, + this.statusCode, + this.subCategoryID, + this.subPolicyNo, + }); + + factory AncillaryOrderProcedureItem.fromJson(Map json) => AncillaryOrderProcedureItem( + ancillaryOrderProcDetailsList: json['AncillaryOrderProcDetailsList'] != null + ? List.from( + (json['AncillaryOrderProcDetailsList'] as List).map( + (x) => AncillaryOrderProcDetail.fromJson(x as Map), + ), + ) + : null, + appointmentDate: DateUtil.convertStringToDate(json['AppointmentDate']), + appointmentNo: json['AppointmentNo'] as int?, + clinicID: json['ClinicID'] as int?, + clinicName: json['ClinicName'] as String?, + companyID: json['CompanyID'] as int?, + companyName: json['CompanyName'] as String?, + doctorID: json['DoctorID'] as int?, + doctorName: json['DoctorName'] as String?, + errCode: json['ErrCode'], + groupID: json['GroupID'] as int?, + insurancePolicyNo: json['InsurancePolicyNo'] as String?, + message: json['Message'] as String?, + patientCardID: json['PatientCardID'] as String?, + patientID: json['PatientID'] as int?, + patientName: json['PatientName'] as String?, + patientType: json['PatientType'] as int?, + policyID: json['PolicyID'] as int?, + policyName: json['PolicyName'] as String?, + projectID: json['ProjectID'] as int?, + setupID: json['SetupID'] as String?, + statusCode: json['StatusCode'] as int?, + subCategoryID: json['SubCategoryID'] as int?, + subPolicyNo: json['SubPolicyNo'] as String?, + ); +} + +class AncillaryOrderProcDetail { + int? approvalLineItemNo; + int? approvalNo; + String? approvalStatus; + int? approvalStatusID; + num? companyShare; + num? companyShareWithTax; + num? companyTaxAmount; + num? discountAmount; + int? discountCategory; + String? discountType; + num? discountTypeValue; + bool? isApprovalCreated; + bool? isApprovalRequired; + dynamic isCheckInAllow; + bool? isCovered; + bool? isLab; + DateTime? orderDate; + int? orderLineItemNo; + int? orderNo; + int? partnerID; + num? partnerShare; + String? partnerShareType; + num? patientShare; + num? patientShareWithTax; + num? patientTaxAmount; + num? procPrice; + int? procedureCategoryID; + String? procedureCategoryName; + String? procedureID; + String? procedureName; + num? taxAmount; + num? taxPct; + + AncillaryOrderProcDetail({ + this.approvalLineItemNo, + this.approvalNo, + this.approvalStatus, + this.approvalStatusID, + this.companyShare, + this.companyShareWithTax, + this.companyTaxAmount, + this.discountAmount, + this.discountCategory, + this.discountType, + this.discountTypeValue, + this.isApprovalCreated, + this.isApprovalRequired, + this.isCheckInAllow, + this.isCovered, + this.isLab, + this.orderDate, + this.orderLineItemNo, + this.orderNo, + this.partnerID, + this.partnerShare, + this.partnerShareType, + this.patientShare, + this.patientShareWithTax, + this.patientTaxAmount, + this.procPrice, + this.procedureCategoryID, + this.procedureCategoryName, + this.procedureID, + this.procedureName, + this.taxAmount, + this.taxPct, + }); + + factory AncillaryOrderProcDetail.fromJson(Map json) => AncillaryOrderProcDetail( + approvalLineItemNo: json['ApprovalLineItemNo'] as int?, + approvalNo: json['ApprovalNo'] as int?, + approvalStatus: json['ApprovalStatus'] as String?, + approvalStatusID: json['ApprovalStatusID'] as int?, + companyShare: _toNum(json['CompanyShare']), + companyShareWithTax: _toNum(json['CompanyShareWithTax']), + companyTaxAmount: _toNum(json['CompanyTaxAmount']), + discountAmount: _toNum(json['DiscountAmount']), + discountCategory: json['DiscountCategory'] as int?, + discountType: json['DiscountType'] as String?, + discountTypeValue: _toNum(json['DiscountTypeValue']), + isApprovalCreated: json['IsApprovalCreated'] as bool?, + isApprovalRequired: json['IsApprovalRequired'] as bool?, + isCheckInAllow: json['IsCheckInAllow'], + isCovered: json['IsCovered'] as bool?, + isLab: json['IsLab'] as bool?, + orderDate: DateUtil.convertStringToDate(json['OrderDate']), + orderLineItemNo: json['OrderLineItemNo'] as int?, + orderNo: json['OrderNo'] as int?, + partnerID: json['PartnerID'] as int?, + partnerShare: _toNum(json['PartnerShare']), + partnerShareType: json['PartnerShareType'] as String?, + patientShare: _toNum(json['PatientShare']), + patientShareWithTax: _toNum(json['PatientShareWithTax']), + patientTaxAmount: _toNum(json['PatientTaxAmount']), + procPrice: _toNum(json['ProcPrice']), + procedureCategoryID: json['ProcedureCategoryID'] as int?, + procedureCategoryName: json['ProcedureCategoryName'] as String?, + procedureID: json['ProcedureID'] as String?, + procedureName: json['ProcedureName'] as String?, + taxAmount: _toNum(json['TaxAmount']), + taxPct: _toNum(json['TaxPct']), + ); +} + +// Helper to safely parse numeric fields that may be int/double/string/null +num? _toNum(dynamic v) { + if (v == null) return null; + if (v is num) return v; + if (v is String) return num.tryParse(v); + return null; +} diff --git a/lib/features/todo_section/todo_section_repo.dart b/lib/features/todo_section/todo_section_repo.dart new file mode 100644 index 0000000..008b22c --- /dev/null +++ b/lib/features/todo_section/todo_section_repo.dart @@ -0,0 +1,379 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class TodoSectionRepo { + Future>>> getOnlineAncillaryOrderList(); + + Future>>> getOnlineAncillaryOrderDetailsProceduresList({ + required int appointmentNoVida, + required int orderNo, + required int projectID, + }); + + Future> checkPaymentStatus({required String transID}); + + Future> createAdvancePayment({ + required int projectID, + required double paymentAmount, + required String paymentReference, + required String paymentMethodName, + required int patientTypeID, + required String patientName, + required int patientID, + required String setupID, + required bool isAncillaryOrder, + }); + + Future> addAdvancedNumberRequest({ + required String advanceNumber, + required String paymentReference, + required int appointmentID, + required int patientID, + required int patientTypeID, + required int patientOutSA, + }); + + Future> autoGenerateAncillaryOrdersInvoice({ + required int orderNo, + required int projectID, + required int appointmentNo, + required List selectedProcedures, + required int languageID, + }); + + Future> applePayInsertRequest({required dynamic applePayInsertRequest}); +} + +class TodoSectionRepoImp implements TodoSectionRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + TodoSectionRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getOnlineAncillaryOrderList() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getOnlineAncillaryOrderList, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List ancillaryOrders = []; + + // Parse the nested structure + if (response['AncillaryOrderList'] != null && response['AncillaryOrderList'] is List) { + final groupsList = response['AncillaryOrderList'] as List; + + // Iterate through each group + for (var group in groupsList) { + if (group is Map && group['AncillaryOrderList'] != null) { + final ordersList = group['AncillaryOrderList'] as List; + final projectName = group['ProjectName'] as String?; + final projectID = group['ProjectID'] as int?; + + // Parse each order item in the group + for (var orderJson in ordersList) { + if (orderJson is Map) { + ancillaryOrders.add(AncillaryOrderItem.fromJson(orderJson, projectName: projectName, projectID: projectID)); + } + } + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: ancillaryOrders, + ); + } catch (e) { + loggerService.logInfo("Error parsing ancillary orders: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in getOnlineAncillaryOrderList: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getOnlineAncillaryOrderDetailsProceduresList({ + required int appointmentNoVida, + required int orderNo, + required int projectID, + }) async { + Map mapDevice = { + 'AppointmentNo_Vida': appointmentNoVida, + 'OrderNo': orderNo, + 'ProjectID': projectID, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getOnlineAncillaryOrderProcList, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List ancillaryOrdersProcedures = []; + + // Parse the flat array structure (NOT nested like AncillaryOrderList) + if (response['AncillaryOrderProcList'] != null && response['AncillaryOrderProcList'] is List) { + final procList = response['AncillaryOrderProcList'] as List; + + // Parse each procedure item directly + for (var procJson in procList) { + if (procJson is Map) { + ancillaryOrdersProcedures.add(AncillaryOrderProcedureItem.fromJson(procJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: ancillaryOrdersProcedures, + ); + } catch (e) { + loggerService.logError("Error parsing ancillary Procedures: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("Unknown error in getOnlineAncillaryOrderDetailsProceduresList: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> checkPaymentStatus({required String transID}) async { + Map mapDevice = {'ClientRequestID': transID}; + + try { + dynamic apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getRequestStatusByRequestID, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = response; + }, + ); + if (failure != null) return Left(failure!); + return Right(apiResponse); + } catch (e) { + loggerService.logError("Unknown error in checkPaymentStatus: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> createAdvancePayment({ + required int projectID, + required double paymentAmount, + required String paymentReference, + required String paymentMethodName, + required int patientTypeID, + required String patientName, + required int patientID, + required String setupID, + required bool isAncillaryOrder, + }) async { + // //VersionID (number) + // // Channel (number) + // // IPAdress (string) + // // generalid (string) + // // LanguageID (number) + // // Latitude (number) + // // Longitude (number) + // // DeviceTypeID (number) + // // PatientType (number) + // // PatientTypeID (number) + // // PatientID (number) + // // PatientOutSA (number) + // // TokenID (string) + // // SessionID (string) + + Map mapDevice = { + 'CustName': patientName, + 'CustID': patientID, + 'SetupID': setupID, + 'ProjectID': projectID, + 'AccountID': patientID, + 'PaymentAmount': paymentAmount, + 'NationalityID': null, + 'DepositorName': patientName, + 'CreatedBy': 3, + 'PaymentMethodName': paymentMethodName, + 'PaymentReference': paymentReference, + 'PaymentMethod': paymentMethodName, + 'IsAncillaryOrder': isAncillaryOrder, + }; + + try { + dynamic apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.createAdvancePayments, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = response; + }, + ); + if (failure != null) return Left(failure!); + return Right(apiResponse); + } catch (e) { + loggerService.logError("Unknown error in createAdvancePayment: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> addAdvancedNumberRequest({ + required String advanceNumber, + required String paymentReference, + required int appointmentID, + required int patientID, + required int patientTypeID, + required int patientOutSA, + }) async { + Map mapDevice = { + 'AdvanceNumber': advanceNumber, + 'PaymentReference': paymentReference, + 'AppointmentID': appointmentID, + 'PatientID': patientID, + 'PatientTypeID': patientTypeID, + 'PatientOutSA': patientOutSA, + }; + + try { + dynamic apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.addAdvanceNumberRequest, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = response; + }, + ); + if (failure != null) return Left(failure!); + return Right(apiResponse); + } catch (e) { + loggerService.logError("Unknown error in addAdvancedNumberRequest: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> autoGenerateAncillaryOrdersInvoice({ + required int orderNo, + required int projectID, + required int appointmentNo, + required List selectedProcedures, + required int languageID, + }) async { + // Extract procedure IDs from selectedProcedures + List procedureOrderIDs = []; + selectedProcedures.forEach((element) { + procedureOrderIDs.add(element["ProcedureID"].toString()); + }); + + Map mapDevice = { + 'LanguageID': languageID, + 'RequestAncillaryOrderInvoice': [ + { + 'MemberID': 102, + 'ProjectID': projectID, + 'AppointmentNo': appointmentNo, + 'OrderNo': orderNo, + 'AncillaryOrderInvoiceProcList': selectedProcedures, + } + ], + 'ProcedureOrderIds': procedureOrderIDs, + }; + + try { + dynamic apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.autoGenerateAncillaryOrdersInvoice, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = response; + }, + ); + if (failure != null) return Left(failure!); + return Right(apiResponse); + } catch (e) { + loggerService.logError("Unknown error in autoGenerateAncillaryOrdersInvoice: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> applePayInsertRequest({required dynamic applePayInsertRequest}) async { + Map mapDevice = { + 'ApplePayInsertRequest': applePayInsertRequest, + }; + + try { + dynamic apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.applePayInsertRequest, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = response; + }, + ); + if (failure != null) return Left(failure!); + return Right(apiResponse); + } catch (e) { + loggerService.logError("Unknown error in applePayInsertRequest: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/todo_section/todo_section_view_model.dart b/lib/features/todo_section/todo_section_view_model.dart new file mode 100644 index 0000000..c0fb96b --- /dev/null +++ b/lib/features/todo_section/todo_section_view_model.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class TodoSectionViewModel extends ChangeNotifier { + TodoSectionRepo todoSectionRepo; + ErrorHandlerService errorHandlerService; + + TodoSectionViewModel({required this.todoSectionRepo, required this.errorHandlerService}); + + initializeTodoSectionViewModel() async { + patientAncillaryOrdersList.clear(); + isAncillaryOrdersLoading = true; + isAncillaryDetailsProceduresLoading = true; + await getPatientOnlineAncillaryOrderList(); + } + + bool isAncillaryOrdersLoading = false; + bool isAncillaryDetailsProceduresLoading = false; + bool isProcessingPayment = false; + List patientAncillaryOrdersList = []; + List patientAncillaryOrderProceduresList = []; + + void setProcessingPayment(bool value) { + isProcessingPayment = value; + notifyListeners(); + } + + Future getPatientOnlineAncillaryOrderList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + patientAncillaryOrdersList.clear(); + isAncillaryOrdersLoading = true; + notifyListeners(); + final result = await todoSectionRepo.getOnlineAncillaryOrderList(); + + result.fold( + (failure) async { + isAncillaryOrdersLoading = false; + await errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientAncillaryOrdersList = apiResponse.data!; + isAncillaryOrdersLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getPatientOnlineAncillaryOrderDetailsProceduresList({ + Function(dynamic)? onSuccess, + Function(String)? onError, + required int appointmentNoVida, + required int orderNo, + required int projectID, + }) async { + isAncillaryDetailsProceduresLoading = true; + notifyListeners(); + + final result = await todoSectionRepo.getOnlineAncillaryOrderDetailsProceduresList( + appointmentNoVida: appointmentNoVida, + orderNo: orderNo, + projectID: projectID, + ); + + result.fold( + (failure) async { + isAncillaryDetailsProceduresLoading = false; + await errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientAncillaryOrderProceduresList = apiResponse.data!; + isAncillaryDetailsProceduresLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future checkPaymentStatus({ + required String transID, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await todoSectionRepo.checkPaymentStatus(transID: transID); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (response) { + if (onSuccess != null) { + onSuccess(response); + } + }, + ); + } + + Future createAdvancePayment({ + required int projectID, + required double paymentAmount, + required String paymentReference, + required String paymentMethodName, + required int patientTypeID, + required String patientName, + required int patientID, + required String setupID, + required bool isAncillaryOrder, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await todoSectionRepo.createAdvancePayment( + projectID: projectID, + paymentAmount: paymentAmount, + paymentReference: paymentReference, + paymentMethodName: paymentMethodName, + patientTypeID: patientTypeID, + patientName: patientName, + patientID: patientID, + setupID: setupID, + isAncillaryOrder: isAncillaryOrder, + ); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (response) { + if (onSuccess != null) { + onSuccess(response); + } + }, + ); + } + + Future addAdvancedNumberRequest({ + required String advanceNumber, + required String paymentReference, + required int appointmentID, + required int patientID, + required int patientTypeID, + required int patientOutSA, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await todoSectionRepo.addAdvancedNumberRequest( + advanceNumber: advanceNumber, + paymentReference: paymentReference, + appointmentID: appointmentID, + patientID: patientID, + patientTypeID: patientTypeID, + patientOutSA: patientOutSA, + ); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (response) { + if (onSuccess != null) { + onSuccess(response); + } + }, + ); + } + + Future autoGenerateAncillaryOrdersInvoice({ + required int orderNo, + required int projectID, + required int appointmentNo, + required List selectedProcedures, + required int languageID, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await todoSectionRepo.autoGenerateAncillaryOrdersInvoice( + orderNo: orderNo, + projectID: projectID, + appointmentNo: appointmentNo, + selectedProcedures: selectedProcedures, + languageID: languageID, + ); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (response) { + if (onSuccess != null) { + onSuccess(response); + } + }, + ); + } + + Future applePayInsertRequest({ + required dynamic applePayInsertRequest, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await todoSectionRepo.applePayInsertRequest(applePayInsertRequest: applePayInsertRequest); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (response) { + if (onSuccess != null) { + onSuccess(response); + } + }, + ); + } +} diff --git a/lib/features/water_monitor/models/insert_user_activity_request_model.dart b/lib/features/water_monitor/models/insert_user_activity_request_model.dart new file mode 100644 index 0000000..a0af649 --- /dev/null +++ b/lib/features/water_monitor/models/insert_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class InsertUserActivityRequestModel { + String? identificationNo; + String? mobileNumber; + num? quantityIntake; + + InsertUserActivityRequestModel({ + this.identificationNo, + this.mobileNumber, + this.quantityIntake, + }); + + Map toJson() { + final map = {}; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (quantityIntake != null) map['QuantityIntake'] = quantityIntake; + return map; + } + + factory InsertUserActivityRequestModel.fromJson(Map json) => InsertUserActivityRequestModel( + identificationNo: json['IdentificationNo']?.toString(), + mobileNumber: json['MobileNumber']?.toString(), + quantityIntake: json['QuantityIntake'], + ); +} + diff --git a/lib/features/water_monitor/models/undo_user_activity_request_model.dart b/lib/features/water_monitor/models/undo_user_activity_request_model.dart new file mode 100644 index 0000000..01dbd92 --- /dev/null +++ b/lib/features/water_monitor/models/undo_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class UndoUserActivityRequestModel { + num? progress; + String? mobileNumber; + String? identificationNo; + + UndoUserActivityRequestModel({ + this.progress, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + if (progress != null) map['Progress'] = progress; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UndoUserActivityRequestModel.fromJson(Map json) => UndoUserActivityRequestModel( + progress: json['Progress'], + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} + diff --git a/lib/features/water_monitor/models/update_user_detail_request_model.dart b/lib/features/water_monitor/models/update_user_detail_request_model.dart new file mode 100644 index 0000000..33dbbb2 --- /dev/null +++ b/lib/features/water_monitor/models/update_user_detail_request_model.dart @@ -0,0 +1,88 @@ +class UpdateUserDetailRequestModel { + // User detail fields + String? activityID; + String? dOB; + String? email; + String? firstName; + String? lastName; + String? firstNameN; + String? middleName; + String? middleNameN; + String? lastNameN; + String? gender; + num? height; + bool? isHeightInCM; + bool? isWeightInKG; + String? zipCode; + num? weight; + bool? isNotificationOn; + String? mobileNumber; + String? identificationNo; + + UpdateUserDetailRequestModel({ + this.activityID, + this.dOB, + this.email, + this.firstName, + this.lastName, + this.firstNameN, + this.middleName, + this.middleNameN, + this.lastNameN, + this.gender, + this.height, + this.isHeightInCM, + this.isWeightInKG, + this.zipCode, + this.weight, + this.isNotificationOn, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + + // User detail fields + if (activityID != null) map['ActivityID'] = activityID; + if (dOB != null) map['DOB'] = dOB; + if (email != null) map['Email'] = email; + if (firstName != null) map['FirstName'] = firstName; + if (lastName != null) map['LastName'] = lastName; + if (firstNameN != null) map['FirstNameN'] = firstNameN; + if (middleName != null) map['MiddleName'] = middleName; + if (middleNameN != null) map['MiddleNameN'] = middleNameN; + if (lastNameN != null) map['LastNameN'] = lastNameN; + if (gender != null) map['Gender'] = gender; + if (height != null) map['Height'] = height; + if (isHeightInCM != null) map['IsHeightInCM'] = isHeightInCM; + if (isWeightInKG != null) map['IsWeightInKG'] = isWeightInKG; + if (zipCode != null) map['ZipCode'] = zipCode; + if (weight != null) map['Weight'] = weight; + if (isNotificationOn != null) map['IsNotificationOn'] = isNotificationOn; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UpdateUserDetailRequestModel.fromJson(Map json) => UpdateUserDetailRequestModel( + activityID: json['ActivityID']?.toString(), + dOB: json['DOB']?.toString(), + email: json['Email']?.toString(), + firstName: json['FirstName']?.toString(), + lastName: json['LastName']?.toString(), + firstNameN: json['FirstNameN']?.toString(), + middleName: json['MiddleName']?.toString(), + middleNameN: json['MiddleNameN']?.toString(), + lastNameN: json['LastNameN']?.toString(), + gender: json['Gender']?.toString(), + height: json['Height'], + isHeightInCM: json['IsHeightInCM'] as bool?, + isWeightInKG: json['IsWeightInKG'] as bool?, + zipCode: json['ZipCode']?.toString(), + weight: json['Weight'], + isNotificationOn: json['IsNotificationOn'] as bool?, + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} diff --git a/lib/features/water_monitor/models/user_progress_models.dart b/lib/features/water_monitor/models/user_progress_models.dart new file mode 100644 index 0000000..667653e --- /dev/null +++ b/lib/features/water_monitor/models/user_progress_models.dart @@ -0,0 +1,111 @@ +/// Model for today's water progress data +class UserProgressForTodayModel { + num? quantityConsumed; + num? percentageConsumed; + num? percentageLeft; + num? quantityLimit; + + UserProgressForTodayModel({ + this.quantityConsumed, + this.percentageConsumed, + this.percentageLeft, + this.quantityLimit, + }); + + factory UserProgressForTodayModel.fromJson(Map json) => UserProgressForTodayModel( + quantityConsumed: json['QuantityConsumed'], + percentageConsumed: json['PercentageConsumed'], + percentageLeft: json['PercentageLeft'], + quantityLimit: json['QuantityLimit'], + ); + + Map toJson() { + return { + 'QuantityConsumed': quantityConsumed, + 'PercentageConsumed': percentageConsumed, + 'PercentageLeft': percentageLeft, + 'QuantityLimit': quantityLimit, + }; + } +} + +/// Model for weekly water progress data +class UserProgressForWeekModel { + int? dayNumber; + String? dayDate; + String? dayName; + num? percentageConsumed; + + UserProgressForWeekModel({ + this.dayNumber, + this.dayDate, + this.dayName, + this.percentageConsumed, + }); + + factory UserProgressForWeekModel.fromJson(Map json) => UserProgressForWeekModel( + dayNumber: json['DayNumber'] as int?, + dayDate: json['DayDate']?.toString(), + dayName: json['DayName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'DayNumber': dayNumber, + 'DayDate': dayDate, + 'DayName': dayName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for monthly water progress data +class UserProgressForMonthModel { + int? monthNumber; + String? monthName; + num? percentageConsumed; + + UserProgressForMonthModel({ + this.monthNumber, + this.monthName, + this.percentageConsumed, + }); + + factory UserProgressForMonthModel.fromJson(Map json) => UserProgressForMonthModel( + monthNumber: json['MonthNumber'] as int?, + monthName: json['MonthName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'MonthNumber': monthNumber, + 'MonthName': monthName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for user progress history data +class UserProgressHistoryModel { + num? quantity; + String? createdDate; + + UserProgressHistoryModel({ + this.quantity, + this.createdDate, + }); + + factory UserProgressHistoryModel.fromJson(Map json) => UserProgressHistoryModel( + quantity: json['Quantity'], + createdDate: json['CreatedDate']?.toString(), + ); + + Map toJson() { + return { + 'Quantity': quantity, + 'CreatedDate': createdDate, + }; + } +} diff --git a/lib/features/water_monitor/models/water_cup_model.dart b/lib/features/water_monitor/models/water_cup_model.dart new file mode 100644 index 0000000..b86fae4 --- /dev/null +++ b/lib/features/water_monitor/models/water_cup_model.dart @@ -0,0 +1,47 @@ +class WaterCupModel { + final String id; + final String name; + final int capacityMl; + final String iconPath; // or use IconData if you prefer + final bool isDefault; + + WaterCupModel({ + required this.id, + required this.name, + required this.capacityMl, + required this.iconPath, + this.isDefault = false, + }); + + WaterCupModel copyWith({ + String? id, + String? name, + int? capacityMl, + String? iconPath, + bool? isDefault, + }) { + return WaterCupModel( + id: id ?? this.id, + name: name ?? this.name, + capacityMl: capacityMl ?? this.capacityMl, + iconPath: iconPath ?? this.iconPath, + isDefault: isDefault ?? this.isDefault, + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'capacityMl': capacityMl, + 'iconPath': iconPath, + 'isDefault': isDefault, + }; + + factory WaterCupModel.fromJson(Map json) => WaterCupModel( + id: json['id'], + name: json['name'], + capacityMl: json['capacityMl'], + iconPath: json['iconPath'], + isDefault: json['isDefault'] ?? false, + ); +} diff --git a/lib/features/water_monitor/water_monitor_repo.dart b/lib/features/water_monitor/water_monitor_repo.dart new file mode 100644 index 0000000..14a74a7 --- /dev/null +++ b/lib/features/water_monitor/water_monitor_repo.dart @@ -0,0 +1,333 @@ +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Progress types to request different ranges from the progress API. +enum ProgressType { today, week, month } + +abstract class WaterMonitorRepo { + /// Fetch user details for water monitoring. + /// The request will include the standard payload injected by ApiClient and + /// additionally these three parameters: Progress, MobileNumber, IdentificationNo. + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }); + + /// Fetch user progress for water monitoring (H2O_GetUserProgress). + Future>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }); + + /// Update user details for water monitoring. + Future>> updateOrInsertUserDetailForWaterMonitoring( + {required UpdateUserDetailRequestModel requestModel, bool isUpdate = false}); + + /// Insert user activity (water intake). + Future>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }); + + /// Undo last user activity. + Future>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }); +} + +class WaterMonitorRepoImp implements WaterMonitorRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + WaterMonitorRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }) async { + Map request = { + "Progress": progress, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserDetail, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract only the specific key from the API response as requested + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }) async { + final progressValue = progressType == ProgressType.today ? 1 : (progressType == ProgressType.week ? 2 : 3); + + Map request = { + "Progress": progressValue, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserProgress, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract progress data and history data + dynamic extracted; + dynamic historyData; + + if (response is Map) { + // Extract history data (available for all progress types) + if (response.containsKey('UserProgressHistoryData')) { + historyData = response['UserProgressHistoryData']; + } + + // Extract progress data based on type + switch (progressType) { + case ProgressType.today: + if (response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } + break; + case ProgressType.week: + if (response.containsKey('UserProgressForWeekData')) { + extracted = response['UserProgressForWeekData']; + } + break; + case ProgressType.month: + if (response.containsKey('UserProgressForMonthData')) { + extracted = response['UserProgressForMonthData']; + } + break; + } + + // fallbacks + if (extracted == null) { + if (response.containsKey('UserProgress')) { + extracted = response['UserProgress']; + } else if (response.containsKey('UserProgressData_New')) { + extracted = response['UserProgressData_New']; + } else { + extracted = response; + } + } + } else { + extracted = response; + } + + // Package both progress data and history data + final combinedData = { + 'progressData': extracted, + 'historyData': historyData, + }; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: combinedData, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateOrInsertUserDetailForWaterMonitoring({ + required UpdateUserDetailRequestModel requestModel, + bool isUpdate = false, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + // Use different endpoint based on isUpdate flag + final endpoint = isUpdate ? ApiConsts.h2oUpdateUserDetail : ApiConsts.h2oInsertUserDetailsNew; + await apiClient.post( + endpoint, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oInsertUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oUndoUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("response h2oUndoUserActivity: ${response.toString()}"); + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart new file mode 100644 index 0000000..01167fe --- /dev/null +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -0,0 +1,1340 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/user_progress_models.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/notification_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WaterMonitorViewModel extends ChangeNotifier { + WaterMonitorRepo waterMonitorRepo; + + WaterMonitorViewModel({required this.waterMonitorRepo}); + + // Controllers + final TextEditingController nameController = TextEditingController(); + final TextEditingController heightController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final TextEditingController ageController = TextEditingController(); + + // Units + final List heightUnits = ['cm', 'm', 'ft', 'in']; + final List weightUnits = ['kg', 'lb']; + + // Selected values + String _selectedHeightUnit = 'cm'; + String _selectedWeightUnit = 'kg'; + String _selectedActivityLevel = 'Lightly active'; + String _selectedNumberOfReminders = '3 Time'; + String _selectedGender = "Male"; + + // ConsumptionScreen + + String _selectedDuration = "Daily"; + bool _isGraphView = true; + + // Validation error message + String? _validationError; + + // Getters + String? get validationError => _validationError; + + String get selectedHeightUnit => _selectedHeightUnit; + + String get selectedWeightUnit => _selectedWeightUnit; + + String get selectedActivityLevel => _selectedActivityLevel; + + String get selectedNumberOfReminders => _selectedNumberOfReminders; + + String get selectedGender => _selectedGender; + + String get selectedDurationFilter => _selectedDuration; + + bool get isGraphView => _isGraphView; + + // Activity level options + List get activityLevels => + ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + + // Reminder options + List get reminderOptions => ["1 Time", "2 Time", "3 Time", "4 Time", "5 Time", "6 Time"]; + + // Gender options + List get genderOptions => ["Male", "Female"]; + +//Duration Options + List get durationFilters => ["Daily", "Weekly", "Monthly"]; + + // Network/data + final AppState _appState = GetIt.instance(); + final NavigationService _navigationService = GetIt.instance(); + final CacheService _cacheService = GetIt.instance(); + + bool _isLoading = false; + dynamic _userDetailData; + bool _isWaterReminderEnabled = false; + + // Progress data lists + List _todayProgressList = []; + List _weekProgressList = []; + List _monthProgressList = []; + List _historyList = []; + + bool get isLoading => _isLoading; + + dynamic get userDetailData => _userDetailData; + + bool get isWaterReminderEnabled => _isWaterReminderEnabled; + + // Getters for progress data + List get todayProgressList => _todayProgressList; + + List get weekProgressList => _weekProgressList; + + List get monthProgressList => _monthProgressList; + + List get historyList => _historyList; + + // Get current progress list based on selected duration + dynamic get currentProgressData { + switch (_selectedDuration) { + case 'Daily': + return _todayProgressList; + case 'Weekly': + return _weekProgressList; + case 'Monthly': + return _monthProgressList; + default: + return _todayProgressList; + } + } + + // Initialize method to be called when needed + Future initialize() async { + _initializeDefaultCups(); + _loadReminderEnabledState(); + await fetchUserDetailsForMonitoring(); + // Fetch daily progress to get consumed amount and daily goal + await fetchUserProgressForMonitoring(); + } + + /// Load reminder enabled state from cache + void _loadReminderEnabledState() { + _isWaterReminderEnabled = _cacheService.getBool(key: CacheConst.waterReminderEnabled) ?? false; + log('Water reminder enabled state loaded: $_isWaterReminderEnabled'); + } + + /// Map selected duration to ProgressType enum + ProgressType _getProgressTypeFromDuration() { + switch (_selectedDuration) { + case 'Daily': + return ProgressType.today; + case 'Weekly': + return ProgressType.week; + case 'Monthly': + return ProgressType.month; + default: + return ProgressType.today; + } + } + + /// Map selected duration to ProgressType enum + int _getProgressIdFromDuration() { + switch (_selectedDuration) { + case 'Daily': + return 1; + case 'Weekly': + return 2; + case 'Monthly': + return 3; + default: + return 1; + } + } + + /// Fetch user progress data based on selected duration + Future fetchUserProgressForMonitoring() async { + try { + _isLoading = true; + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst(RegExp(r'^0'), ''); + final identification = authenticated.patientIdentificationNo ?? ''; + final progressType = _getProgressTypeFromDuration(); + + final result = await waterMonitorRepo.getUserProgressForWaterMonitoring( + progressType: progressType, + mobileNumber: mobile, + identificationNo: identification, + ); + + result.fold((failure) { + log('Error fetching user progress: ${failure.message}'); + }, (apiModel) { + log("User Progress Data ($_selectedDuration): ${apiModel.data.toString()}"); + + // Parse the response based on progress type + try { + // Extract progressData and historyData from combined response + dynamic progressData; + dynamic historyData; + + if (apiModel.data is Map && apiModel.data.containsKey('progressData')) { + progressData = apiModel.data['progressData']; + historyData = apiModel.data['historyData']; + } else { + // Fallback to old structure + progressData = apiModel.data; + } + + // Parse history data (available for all progress types, especially for daily) + if (historyData != null && historyData is List) { + _historyList.clear(); + for (var item in historyData) { + if (item is Map) { + _historyList.add(UserProgressHistoryModel.fromJson(item as Map)); + } + } + log('History data parsed: ${_historyList.length} items'); + } + + if (progressData != null && progressData is List) { + switch (progressType) { + case ProgressType.today: + _todayProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _todayProgressList.add(UserProgressForTodayModel.fromJson(item as Map)); + } + } + + // Update consumed amount and daily goal from API response + if (_todayProgressList.isNotEmpty) { + final todayData = _todayProgressList.first; + if (todayData.quantityConsumed != null) { + _totalConsumedMl = todayData.quantityConsumed!.toInt(); + log('Updated consumed from API: $_totalConsumedMl ml'); + } + if (todayData.quantityLimit != null) { + _dailyGoalMl = todayData.quantityLimit!.toInt(); + log('Updated daily goal from API: $_dailyGoalMl ml'); + } + } + + break; + + case ProgressType.week: + _weekProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _weekProgressList.add(UserProgressForWeekModel.fromJson(item as Map)); + } + } + log('Week Progress: ${_weekProgressList.length} items'); + break; + + case ProgressType.month: + _monthProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _monthProgressList.add(UserProgressForMonthModel.fromJson(item as Map)); + } + } + log('Month Progress: ${_monthProgressList.length} items'); + break; + } + } + } catch (e) { + log('Error parsing progress data: $e'); + } + }); + } catch (e) { + log('Exception in fetchUserProgressForMonitoring: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future fetchUserDetailsForMonitoring({Function(dynamic)? onSuccess, Function(String)? onError}) async { + try { + _isLoading = true; + + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + if (onError != null) onError('User not authenticated'); + return; + } + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', ''); + final identification = authenticated.patientIdentificationNo ?? ''; + + final result = await waterMonitorRepo.getUserDetailsForWaterMonitoring( + progress: 1, + mobileNumber: mobile, + identificationNo: identification, + ); + + result.fold((failure) { + _userDetailData = null; + if (onError != null) onError(failure.message); + }, (apiModel) { + _userDetailData = apiModel.data; + + // Populate form fields from the fetched data + if (_userDetailData != null) { + _populateFormFields(_userDetailData); + } + + if (onSuccess != null) onSuccess(_userDetailData); + }); + } catch (e) { + _userDetailData = null; + if (onError != null) onError(e.toString()); + } finally { + _isLoading = false; + notifyListeners(); + + if (_userDetailData == null) { + try { + _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsScreen); + } catch (navErr) { + log('Navigation to water monitor settings failed: $navErr'); + } + } + } + } + + /// Populates form fields from the API response data + void _populateFormFields(dynamic data) { + if (data == null) return; + + try { + // Parse the response and populate fields + if (data is Map) { + // Name + if (data['FirstName'] != null) { + nameController.text = data['FirstName'].toString(); + } + + // Gender + if (data['Gender'] != null) { + final gender = data['Gender'].toString(); + if (gender == 'M' || gender == 'Male') { + _selectedGender = 'Male'; + } else if (gender == 'F' || gender == 'Female') { + _selectedGender = 'Female'; + } + } + + // Age - calculate from DOB if available + if (data['DOB'] != null) { + final dob = data['DOB'].toString(); + final age = _calculateAgeFromDOB(dob); + if (age > 0) { + ageController.text = age.toString(); + } + } + + // Height + if (data['Height'] != null) { + heightController.text = data['Height'].toString(); + // Set height unit + if (data['IsHeightInCM'] != null) { + _selectedHeightUnit = data['IsHeightInCM'] == true ? 'cm' : 'in'; + } + } + + // Weight + if (data['Weight'] != null) { + weightController.text = data['Weight'].toString(); + // Set weight unit + if (data['IsWeightInKG'] != null) { + _selectedWeightUnit = data['IsWeightInKG'] == true ? 'kg' : 'lb'; + } + } + + // Activity Level - map ActivityID to activity level + if (data['ActivityID'] != null) { + final activityId = data['ActivityID'].toString(); + _selectedActivityLevel = _getActivityLevelFromID(activityId); + } + + // Number of reminders (if available in response) + // Note: This may not be in the response, keeping default if not present + + notifyListeners(); + } + } catch (e) { + log('Error populating form fields: $e'); + } + } + + /// Calculate age from DOB string in format /Date(milliseconds+0300)/ + int _calculateAgeFromDOB(String dobString) { + try { + // Parse the /Date(milliseconds+0300)/ format + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dobString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + final dob = DateTime.fromMillisecondsSinceEpoch(milliseconds); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + return age; + } + } + } catch (e) { + log('Error calculating age from DOB: $e'); + } + return 0; + } + + /// Map activity ID to activity level string + String _getActivityLevelFromID(String activityId) { + switch (activityId) { + case '1': + return "Almost Inactive (no exercise)"; + case '2': + return "Lightly active"; + case '3': + return "Lightly active (1-3) days per week"; + case '4': + return "Super active (very hard exercise)"; + default: + return "Lightly active"; + } + } + + /// Populate form fields from authenticated user data (for new users) + void populateFromAuthenticatedUser() { + try { + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) return; + + // Name - use firstName if available + if (authenticated.firstName != null && authenticated.firstName!.isNotEmpty) { + nameController.text = authenticated.firstName!; + } + + // Gender - map from patient gender + if (authenticated.gender != null) { + final gender = (authenticated.gender == 1 ? 'male' : 'female').toLowerCase(); + if (gender.contains('m') || gender == 'male') { + _selectedGender = 'Male'; + } else if (gender.contains('f') || gender == 'female') { + _selectedGender = 'Female'; + } + } + + // Age - calculate from DOB if available + if (authenticated.dateofBirth != null && authenticated.dateofBirth!.isNotEmpty) { + final age = _calculateAgeFromDOB(authenticated.dateofBirth!); + if (age > 0) { + ageController.text = age.toString(); + } + } + + // Set default units and activity level + _selectedHeightUnit = 'cm'; + _selectedWeightUnit = 'kg'; + _selectedActivityLevel = 'Lightly active'; + _selectedNumberOfReminders = '3 Time'; + + log('Form fields populated from authenticated user: ${authenticated.firstName}'); + notifyListeners(); + } catch (e) { + log('Error populating from authenticated user: $e'); + } + } + + // Reset all fields to default + void resetFields() { + nameController.clear(); + heightController.clear(); + weightController.clear(); + ageController.clear(); + _selectedHeightUnit = 'cm'; + _selectedWeightUnit = 'kg'; + _selectedActivityLevel = 'Lightly active'; + _selectedNumberOfReminders = '3 Time'; + _selectedGender = "Male"; + notifyListeners(); + } + + // Setters with notification + void setFilterDuration(String duration) async { + _selectedDuration = duration; + notifyListeners(); + // Fetch new progress data when duration filter changes + await fetchUserProgressForMonitoring(); + } + + // Setters with notification + void setGraphView(bool value) { + _isGraphView = value; + notifyListeners(); + } + + // Setters with notification + void setGender(String gender) { + _selectedGender = gender; + notifyListeners(); + } + + void setHeightUnit(String unit) { + _selectedHeightUnit = unit; + notifyListeners(); + } + + void setWeightUnit(String unit) { + _selectedWeightUnit = unit; + notifyListeners(); + } + + void setActivityLevel(String level) { + _selectedActivityLevel = level; + notifyListeners(); + } + + void setNumberOfReminders(String number) { + _selectedNumberOfReminders = number; + notifyListeners(); + } + + // Validation methods + bool get isValid { + _validationError = null; // Clear previous error + + if (nameController.text.trim().isEmpty) { + _validationError = 'Name is required'; + return false; + } + + if (ageController.text.trim().isEmpty) { + _validationError = 'Age is required'; + return false; + } + + if (!_isAgeValid()) { + _validationError = validateAge(); + return false; + } + + if (heightController.text.trim().isEmpty) { + _validationError = 'Height is required'; + return false; + } + + if (!_isHeightValid()) { + _validationError = validateHeight(); + return false; + } + + if (weightController.text.trim().isEmpty) { + _validationError = 'Weight is required'; + return false; + } + + if (!_isWeightValid()) { + _validationError = validateWeight(); + return false; + } + + return true; + } + + bool _isAgeValid() { + final age = int.tryParse(ageController.text.trim()); + return age != null && age >= 11 && age <= 120; + } + + bool _isHeightValid() { + final height = double.tryParse(heightController.text.trim()); + return height != null && height > 0; + } + + bool _isWeightValid() { + final weight = double.tryParse(weightController.text.trim()); + return weight != null && weight > 0; + } + + String? validateAge() { + if (ageController.text.trim().isEmpty) { + return 'Age is required'.needTranslation; + } + final age = int.tryParse(ageController.text.trim()); + if (age == null) { + return 'Invalid age'.needTranslation; + } + if (age < 11 || age > 120) { + return 'Age must be between 11 and 120'.needTranslation; + } + return null; + } + + String? validateHeight() { + if (heightController.text.trim().isEmpty) { + return 'Height is required'.needTranslation; + } + final height = double.tryParse(heightController.text.trim()); + if (height == null || height <= 0) { + return 'Invalid height'.needTranslation; + } + return null; + } + + String? validateWeight() { + if (weightController.text.trim().isEmpty) { + return 'Weight is required'.needTranslation; + } + final weight = double.tryParse(weightController.text.trim()); + if (weight == null || weight <= 0) { + return 'Invalid weight'.needTranslation; + } + return null; + } + + String _getApiCompatibleGender() { + if (_selectedGender == "Female") { + return "F"; + } + return "M"; + } + + // Save settings + Future saveSettings() async { + if (!isValid) { + notifyListeners(); // Notify so error can be read + return false; + } + + try { + _isLoading = true; + _validationError = null; + notifyListeners(); + + // Determine if this is an update or insert + final isUpdate = _userDetailData != null; + + // Get authenticated user for mobile number and identification number + final authenticated = _appState.getAuthenticatedUser(); + final mobile = (authenticated?.mobileNumber?.replaceAll('+', '') ?? '').replaceFirst("0", ""); + final identification = authenticated?.patientIdentificationNo ?? ''; + final zipCode = authenticated?.zipCode ?? '966'; + final email = authenticated?.emailAddress ?? ''; + + // Get activity ID based on selected activity level + String activityID = _getActivityID(); + + // Create request model with user detail fields only + final requestModel = UpdateUserDetailRequestModel( + firstName: nameController.text.trim(), + lastName: nameController.text.trim(), + firstNameN: nameController.text.trim(), + middleName: '', + middleNameN: '', + lastNameN: nameController.text.trim(), + gender: _getApiCompatibleGender(), + dOB: _calculateDOBFromAge(), + height: double.tryParse(heightController.text.trim()), + isHeightInCM: _selectedHeightUnit == 'cm', + weight: double.tryParse(weightController.text.trim()), + isWeightInKG: _selectedWeightUnit == 'kg', + activityID: activityID, + isNotificationOn: true, + mobileNumber: mobile, + identificationNo: identification, + email: email, + zipCode: zipCode, + ); + + // Call the API + final result = await waterMonitorRepo.updateOrInsertUserDetailForWaterMonitoring( + requestModel: requestModel, + isUpdate: isUpdate, + ); + + return result.fold( + (failure) { + _validationError = failure.message; + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) async { + // Update local data with response + _userDetailData = apiModel.data; + // Fetch daily progress to get the updated goal and consumed data from API + await fetchUserProgressForMonitoring(); + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + _validationError = e.toString(); + _isLoading = false; + notifyListeners(); + return false; + } + } + + // Helper method to get activity ID based on activity level + String _getActivityID() { + switch (_selectedActivityLevel) { + case "Almost Inactive (no exercise)": + return "1"; + case "Lightly active": + return "2"; + case "Lightly active (1-3) days per week": + return "3"; + case "Super active (very hard exercise)": + return "4"; + default: + return "2"; + } + } + + // Helper method to calculate DOB from age + String _calculateDOBFromAge() { + final age = int.tryParse(ageController.text.trim()) ?? 0; + if (age > 0) { + final currentYear = DateTime.now().year; + final birthYear = currentYear - age; + // Create a DateTime for January 1st of the birth year + final birthDate = DateTime(birthYear, 1, 1); + // Convert to API format: /Date(milliseconds+0300)/ using DateUtil + return DateUtil.convertDateToString(birthDate); + } + return ""; + } + + @override + void dispose() { + nameController.dispose(); + heightController.dispose(); + weightController.dispose(); + ageController.dispose(); + super.dispose(); + } + + List _cups = []; + String? _selectedCupId; + int _totalConsumedMl = 0; // Loaded from API + int _dailyGoalMl = 0; // Loaded from API + + // Calibration: portion of the bottle SVG height that is fillable (0.0 - 1.0) + double _fillableHeightPercent = 0.7; + + List get cups => _cups; + + WaterCupModel? get selectedCup { + if (_cups.isEmpty) return null; + return _cups.firstWhere((c) => c.id == _selectedCupId, orElse: () => _cups.first); + } + + int get totalConsumedMl => _totalConsumedMl; + + int get dailyGoalMl => _dailyGoalMl; + + // Portion of bottle drawable height that is fillable. + double get fillableHeightPercent => _fillableHeightPercent; + + void setFillableHeightPercent(double v) { + _fillableHeightPercent = v.clamp(0.0, 1.0); + notifyListeners(); + } + + // Normalized progress in 0.0 - 1.0 (ensure double) + double get progress { + if (_dailyGoalMl == 0) return 0.0; + final p = _totalConsumedMl / _dailyGoalMl; + return p.clamp(0.0, 1.0).toDouble(); + } + + // Convenience percent (0 - 100) + double get progressPercent => (progress * 100.0).clamp(0.0, 100.0).toDouble(); + + // Calculate hydration status based on progress + String get hydrationStatus { + final percent = progressPercent; + + if (percent >= 90) { + return "Well Hydrated"; + } else if (percent >= 70) { + return "Hydrated"; + } else if (percent >= 50) { + return "Moderately Hydrated"; + } else if (percent >= 30) { + return "Slightly Dehydrated"; + } else { + return "Dehydrated"; + } + } + + // Get hydration status color + Color get hydrationStatusColor { + final percent = progressPercent; + + if (percent >= 90) { + return AppColors.successColor; // Dark Green + } else if (percent >= 70) { + return AppColors.successColor; // Green + } else if (percent >= 50) { + return AppColors.warningColorYellow; //orange + } else if (percent >= 30) { + return AppColors.warningColorYellow; // Orange + } else { + return AppColors.errorColor; // Red + } + } + + String get nextDrinkTime { + if (progressPercent >= 100) { + return "Goal Achieved!"; + } + + // Get number of reminders from selected string (e.g., "3 Time" -> 3) + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + + // Define waking hours (e.g., 6 AM to 10 PM = 16 hours) + const wakingHoursStart = 6; // 6 AM + const wakingHoursEnd = 22; // 10 PM + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + + // Calculate interval between drinks in hours + final intervalHours = totalWakingHours / remindersPerDay; + + // Get current time + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + // If before waking hours, next drink is at start time + if (currentHour < wakingHoursStart) { + return "${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + if (currentHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Calculate which interval we're in + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + + // Calculate next drink time + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + // If next drink time is past bedtime, show tomorrow + if (nextDrinkHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Format the time + final hour = nextDrinkHour.floor(); + final minute = ((nextDrinkHour - hour) * 60).round(); + + // Convert to 12-hour format + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + + return "${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period"; + } + + /// Get time until next drink in minutes + int get minutesUntilNextDrink { + if (progressPercent >= 100) return 0; + + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + const wakingHoursStart = 6; + const wakingHoursEnd = 22; + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + final intervalHours = totalWakingHours / remindersPerDay; + + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + if (currentHour < wakingHoursStart) { + final minutesUntil = ((wakingHoursStart - currentHour) * 60).round(); + return minutesUntil; + } + + if (currentHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + if (nextDrinkHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final minutesUntil = ((nextDrinkHour - currentHour) * 60).round(); + return minutesUntil.clamp(0, 1440); + } + + // Allow updating consumed and goal through the vm so UI doesn't manipulate internal fields directly + void setTotalConsumedMl(int ml) { + _totalConsumedMl = ml; + notifyListeners(); + } + + void addConsumedMl(int ml) { + _totalConsumedMl = (_totalConsumedMl + ml).clamp(0, 1000000); + notifyListeners(); + } + + void subtractConsumedMl(int ml) { + _totalConsumedMl = (_totalConsumedMl - ml).clamp(0, 1000000); + notifyListeners(); + } + + void setDailyGoal(int ml) { + _dailyGoalMl = ml; + notifyListeners(); + } + + void _initializeDefaultCups() { + _cups = [ + WaterCupModel( + id: 'default_125', + name: '125ml', + capacityMl: 125, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_150', + name: '150ml', + capacityMl: 150, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_175', + name: '175ml', + capacityMl: 175, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_200', + name: '200ml', + capacityMl: 200, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_250', + name: '250ml', + capacityMl: 250, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_300', + name: '300ml', + capacityMl: 300, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + ]; + _selectedCupId = _cups.first.id; + notifyListeners(); + } + + void selectCup(String cupId) { + _selectedCupId = cupId; + notifyListeners(); + } + + void addCup(WaterCupModel cup) { + _cups.add(cup); + notifyListeners(); + } + + void updateCup(WaterCupModel updatedCup) { + final index = _cups.indexWhere((c) => c.id == updatedCup.id); + if (index != -1) { + _cups[index] = updatedCup; + notifyListeners(); + } + } + + // Public alias for deleting/removing a cup. Keeps API intention clear in UI code. + void removeCup(String cupId) { + deleteCup(cupId); + } + + void deleteCup(String cupId) { + final cup = _cups.firstWhere((c) => c.id == cupId); + if (cup.isDefault) return; // can't delete default cups + + _cups.removeWhere((c) => c.id == cupId); + if (_selectedCupId == cupId) { + _selectedCupId = _cups.first.id; + } + notifyListeners(); + } + + // Returns the currently selected cup capacity in ml (0 if none) + int get selectedCupCapacityMl => selectedCup?.capacityMl ?? 0; + + /// Increment the consumed amount by the currently selected cup capacity. + /// This centralizes business logic here so UI just calls this method. + void incrementBySelectedCup() { + if (selectedCup != null) { + addConsumedMl(selectedCup!.capacityMl); + } + } + + /// Decrement the consumed amount by the currently selected cup capacity. + /// Ensures value is clamped inside the VM (subtractConsumedMl already clamps). + void decrementBySelectedCup() { + if (selectedCup != null) { + subtractConsumedMl(selectedCup!.capacityMl); + } + } + + /// Insert user activity (record water intake) + Future insertUserActivity({required int quantityIntake}) async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = InsertUserActivityRequestModel( + identificationNo: identification, + mobileNumber: mobile, + quantityIntake: quantityIntake, + ); + + // Call the API + final result = await waterMonitorRepo.insertUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + log('Error inserting user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + log("Insert user activity success: ${apiModel.data.toString()}"); + // Update consumed amount and goal from the response + if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { + final progressData = (apiModel.data as List).first; + if (progressData is Map) { + // Update consumed amount + if (progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + log('Updated consumed after insert: $_totalConsumedMl ml'); + } + } + // Update daily goal + if (progressData.containsKey('QuantityLimit')) { + final limit = progressData['QuantityLimit']; + if (limit != null) { + _dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl; + log('Updated daily goal after insert: $_dailyGoalMl ml'); + } + } + } + + // Refresh progress data to ensure consistency + fetchUserProgressForMonitoring(); + } + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in insertUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// Undo last user activity + Future undoUserActivity() async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = UndoUserActivityRequestModel( + progress: _getProgressIdFromDuration(), + mobileNumber: mobile, + identificationNo: identification, + ); + + // Call the API + final result = await waterMonitorRepo.undoUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + log('Error undoing user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + log("Undo user activity success: ${apiModel.data.toString()}"); + + // Update consumed amount and goal from the response + if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { + final progressData = (apiModel.data as List).first; + if (progressData is Map) { + // Update consumed amount + if (progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + log('Updated consumed after undo: $_totalConsumedMl ml'); + } + } + // Update daily goal + if (progressData.containsKey('QuantityLimit')) { + final limit = progressData['QuantityLimit']; + if (limit != null) { + _dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl; + log('Updated daily goal after undo: $_dailyGoalMl ml'); + } + } + } + } + fetchUserProgressForMonitoring(); + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in undoUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// Schedule water reminders based on user's reminder settings + Future scheduleWaterReminders() async { + try { + final notificationService = getIt.get(); + + // Request permission first + final hasPermission = await notificationService.requestPermissions(); + if (!hasPermission) { + log('Notification permission denied'); + return false; + } + + // Calculate reminder times based on _selectedNumberOfReminders + final reminderTimes = _calculateReminderTimes(); + + if (reminderTimes.isEmpty) { + log('No reminder times calculated'); + return false; + } + + // Schedule water reminders + await notificationService.scheduleWaterReminders( + reminderTimes: reminderTimes, + title: 'Time to Drink Water! 💧'.needTranslation, + body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation, + ); + + // Save reminder enabled state to cache + _isWaterReminderEnabled = true; + await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: true); + + log('Scheduled ${reminderTimes.length} water reminders successfully'); + notifyListeners(); + return true; + } catch (e) { + log('Exception in scheduleWaterReminders: $e'); + return false; + } + } + + /// Calculate reminder times based on selected number of reminders + List _calculateReminderTimes() { + try { + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + + const wakingHoursStart = 6; // 6 AM + const wakingHoursEnd = 22; // 10 PM + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + + final intervalHours = totalWakingHours / remindersPerDay; + + List times = []; + final now = DateTime.now(); + + for (int i = 0; i < remindersPerDay; i++) { + final hourDecimal = wakingHoursStart + (i * intervalHours); + final hour = hourDecimal.floor(); + final minute = ((hourDecimal - hour) * 60).round(); + + final reminderTime = DateTime( + now.year, + now.month, + now.day, + hour, + minute, + ); + + times.add(reminderTime); + } + + return times; + } catch (e) { + log('Error calculating reminder times: $e'); + return []; + } + } + + /// Cancel all water reminders + Future cancelWaterReminders() async { + try { + final notificationService = GetIt.instance(); + + // Get pending notifications and cancel water reminders (IDs 5000-5999) + final pendingNotifications = await notificationService.getPendingNotifications(); + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + await notificationService.cancelNotification(notification.id); + } + } + + // Save reminder disabled state to cache + _isWaterReminderEnabled = false; + await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: false); + + log('Cancelled all water reminders'); + notifyListeners(); + return true; + } catch (e) { + log('Exception in cancelWaterReminders: $e'); + return false; + } + } + + /// Get list of scheduled water reminder times + Future> getScheduledReminderTimes() async { + try { + final notificationService = GetIt.instance(); + final pendingNotifications = await notificationService.getPendingNotifications(); + + List times = []; + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + // Note: PendingNotificationRequest doesn't contain scheduled time + // We can only return the calculated times based on current settings + times = _calculateReminderTimes(); + break; + } + } + + return times; + } catch (e) { + log('Exception in getScheduledReminderTimes: $e'); + return []; + } + } + + /// Schedule a test notification after 5 seconds + /// Useful for testing notification functionality + Future scheduleTestNotification() async { + try { + final notificationService = GetIt.instance(); + + // Request permission first + final hasPermission = await notificationService.requestPermissions(); + if (!hasPermission) { + log('Notification permission denied for test notification'); + return false; + } + + // Schedule notification 5 seconds from now + final scheduledTime = DateTime.now().add(const Duration(seconds: 5)); + + await notificationService.scheduleNotification( + id: 9999, + // Use a unique ID for test notifications + title: 'Time to Drink Water! 💧'.needTranslation, + body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation, + scheduledDate: scheduledTime, + payload: 'test_notification', + ); + + log('Test notification scheduled for 5 seconds from now'); + return true; + } catch (e) { + log('Exception in scheduleTestNotification: $e'); + return false; + } + } +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 7433ff7..ea0bcb8 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const english = 'english'; static const arabic = 'arabic'; static const login = 'login'; @@ -189,7 +189,6 @@ abstract class LocaleKeys { static const firstName = 'firstName'; static const middleName = 'middleName'; static const lastName = 'lastName'; - static const female = 'female'; static const preferredLanguage = 'preferredLanguage'; static const locationsRegister = 'locationsRegister'; static const ksa = 'ksa'; @@ -876,4 +875,6 @@ abstract class LocaleKeys { static const walkin = 'walkin'; static const laserClinic = 'laserClinic'; static const continueString = 'continueString'; + static const covid_info = 'covid_info'; + } diff --git a/lib/main.dart b/lib/main.dart index 6426244..eb57c37 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,22 +9,33 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -63,7 +74,7 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - AppDependencies.addDependencies(); + await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); @@ -101,6 +112,9 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), ), @@ -130,8 +144,39 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ),ChangeNotifierProvider( + ), + ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), @@ -154,11 +199,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Dr. AlHabib', builder: (context, mchild) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(1.0), - ), - child: mchild!); + return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)), child: mchild!); }, showSemanticsDebugger: false, debugShowCheckedModeBanner: false, diff --git a/lib/presentation/allergies/allergies_list_page.dart b/lib/presentation/allergies/allergies_list_page.dart new file mode 100644 index 0000000..efcdd0a --- /dev/null +++ b/lib/presentation/allergies/allergies_list_page.dart @@ -0,0 +1,138 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; + +class AllergiesListPage extends StatelessWidget { + AllergiesListPage({super.key}); + + late MedicalFileViewModel medicalFileViewModel; + + @override + Widget build(BuildContext context) { + medicalFileViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.allergies.tr(), + child: SingleChildScrollView( + child: Consumer(builder: (context, medicalFileVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ListView.separated( + scrollDirection: Axis.vertical, + itemCount: medicalFileVM.isPatientAllergiesListLoading + ? 5 + : medicalFileVM.patientAllergiesList.isNotEmpty + ? medicalFileVM.patientAllergiesList.length + : 1, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(left: 24.h, right: 24.h), + itemBuilder: (context, index) { + return medicalFileVM.isPatientAllergiesListLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain).toShimmer2(isShow: true), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + // AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + // AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ) + : medicalFileVM.patientAllergiesList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (medicalFileVM.patientAllergiesList[index].description).toString().toText16(isBold: true).toShimmer2(isShow: false), + (medicalFileVM.patientAllergiesList[index].remarks).toString().toText12(), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ) + : Utils.getNoDataWidget(context, noDataText: "No allergies data found...".needTranslation); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 60.h), + ], + ); + }), + ), + ), + ); + } +} diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index bac44dd..bd81584 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -2,38 +2,44 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/patient_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart'; -import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:maps_launcher/maps_launcher.dart'; import 'package:provider/provider.dart'; -import '../../core/dependencies.dart'; import '../medical_file/widgets/medical_file_card.dart'; class AppointmentDetailsPage extends StatefulWidget { @@ -49,24 +55,32 @@ class _AppointmentDetailsPageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; late PrescriptionsViewModel prescriptionsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + late ContactUsViewModel contactUsViewModel; + late LabViewModel labViewModel; + late RadiologyViewModel radiologyViewModel; @override void initState() { - scheduleMicrotask(() { - if (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { - prescriptionsViewModel.setPrescriptionsDetailsLoading(); - prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel()); - } + scheduleMicrotask(() async { + CalenderUtilsNew calendarUtils = await CalenderUtilsNew.instance; + var doesExist = await calendarUtils.checkIfEventExist("${widget.patientAppointmentHistoryResponseModel.appointmentNo}"); + myAppointmentsViewModel.setAppointmentReminder(doesExist, widget.patientAppointmentHistoryResponseModel); + setState((){ + + }); + }); super.initState(); } @override Widget build(BuildContext context) { - AppState appState = getIt.get(); myAppointmentsViewModel = Provider.of(context, listen: false); prescriptionsViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); + contactUsViewModel = Provider.of(context, listen: false); + labViewModel = Provider.of(context, listen: false); + radiologyViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -74,16 +88,54 @@ class _AppointmentDetailsPageState extends State { Expanded( child: CollapsingListView( title: "Appointment Details".needTranslation, - report: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? () {} : null, + report: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + ? () { + contactUsViewModel.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel); + Navigator.of(context).push( + CustomPageRoute( + page: FeedbackPage(), + ), + ); + } + : null, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppointmentDoctorCard( patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, - onAskDoctorTap: () {}, + onAskDoctorTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); + await myAppointmentsViewModel.isDoctorAvailable( + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + doctorId: widget.patientAppointmentHistoryResponseModel.doctorID, + clinicId: widget.patientAppointmentHistoryResponseModel.clinicID, + onSuccess: (value) async { + if (value) { + print("Doctor is available"); + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + print("Doctor is not available"); + } + }); + }, onCancelTap: () async { myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + LoaderBottomSheet.showLoader(loadingText: "Cancelling Appointment, Please Wait...".needTranslation); await myAppointmentsViewModel.cancelAppointment( patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, @@ -103,6 +155,10 @@ class _AppointmentDetailsPageState extends State { isFullScreen: false, ); }); + var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + setState(() { + myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + }); }, onRescheduleTap: () async { openDoctorScheduleCalendar(); @@ -112,90 +168,105 @@ class _AppointmentDetailsPageState extends State { !AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? Column( children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - "Appointment Status".needTranslation.toText16(isBold: true), - ], - ), - SizedBox(height: 4.h), - (!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel) - ? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500) - : "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)), - SizedBox(height: 16.h), - //TODO Add countdown timer in case of LiveCare Appointment - widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false - ? Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "The doctor will call you once the appointment time approaches." - .needTranslation - .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), - ], + Consumer(builder: (context, bookAppointmentsVM, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Appointment Status".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(height: 4.h), + (!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel) + ? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500) + : "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)), + SizedBox(height: 16.h), + //TODO Add countdown timer in case of LiveCare Appointment + widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false + ? Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "The doctor will call you once the appointment time approaches." + .needTranslation + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), ), - ), - ], - ) - : Stack( - children: [ - ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(24.r), - // Todo: what is this???? Api Key??? 😲 - child: Image.network( - "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng", - fit: BoxFit.contain, + ], + ) + : Stack( + children: [ + ClipRRect( + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(24.r), + // Todo: what is this???? Api Key??? 😲 + child: Image.network( + "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}", + fit: BoxFit.contain, + ), ), - ), - Positioned( - bottom: 0, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.785, - child: CustomButton( - text: "Get Directions".needTranslation, - onPressed: () { - MapsLauncher.launchCoordinates( - double.parse(widget.patientAppointmentHistoryResponseModel.latitude!), - double.parse(widget.patientAppointmentHistoryResponseModel.longitude!), - widget.patientAppointmentHistoryResponseModel.projectName); - }, - backgroundColor: AppColors.textColor.withValues(alpha: 0.8), - borderColor: AppointmentType.getNextActionButtonColor( - widget.patientAppointmentHistoryResponseModel.nextAction) - .withValues(alpha: 0.01), - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - icon: AppAssets.directions_icon, - iconColor: AppColors.whiteColor, - iconSize: 14.h, - ).paddingAll(12.h), + Positioned( + bottom: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.785, + child: CustomButton( + text: "Get Directions".needTranslation, + onPressed: () { + MapsLauncher.launchCoordinates(double.parse(widget.patientAppointmentHistoryResponseModel.latitude!), + double.parse(widget.patientAppointmentHistoryResponseModel.longitude!), widget.patientAppointmentHistoryResponseModel.projectName); + }, + backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppAssets.directions_icon, + iconColor: AppColors.whiteColor, + iconSize: 14.h, + ).paddingAll(12.h), + ), ), - ), - ], - ), - ], + ], + ), + SizedBox(height: 8.h), + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null ? Wrap( + direction: Axis.horizontal, + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" + : "Floor: ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", + ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + AppCustomChipWidget( + labelText: + "Nearest Gate: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") + .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + ], + ) : SizedBox.shrink(), + ], + ), ), - ), - ), + ); + }), SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -219,13 +290,57 @@ class _AppointmentDetailsPageState extends State { ), const Spacer(), Switch( - activeThumbColor: AppColors.successColor, + // activeThumbColor: AppColors.successColor, activeTrackColor: AppColors.successColor.withValues(alpha: .15), value: widget.patientAppointmentHistoryResponseModel.hasReminder!, - onChanged: (newValue) { - setState(() { - myAppointmentsViewModel.setAppointmentReminder(newValue, widget.patientAppointmentHistoryResponseModel); - }); + onChanged: (newValue) async { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + bool isEventAddedOrRemoved = false; + if(newValue == true){ + DateTime startDate = DateTime.now(); + DateTime endDate = DateUtil.convertStringToDate(widget + .patientAppointmentHistoryResponseModel.appointmentDate); + BottomSheetUtils().showReminderBottomSheet( + context, + endDate, + widget.patientAppointmentHistoryResponseModel.doctorNameObj??"", + "${widget.patientAppointmentHistoryResponseModel.appointmentNo}"??"", + "", + "", + title: "Appointment with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}".needTranslation, + description:"${widget.patientAppointmentHistoryResponseModel.doctorNameObj} will be having an appointment on ${widget.patientAppointmentHistoryResponseModel.appointmentDate}".needTranslation, + onSuccess: () { + setState(() { + myAppointmentsViewModel.setAppointmentReminder(newValue, widget.patientAppointmentHistoryResponseModel); + }); + }, + isMultiAllowed: true, + onMultiDateSuccess: (int selectedIndex) async { + + isEventAddedOrRemoved = await calender.createOrUpdateEvent( + title: "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget + .patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}".needTranslation, + description: "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} in ${widget + .patientAppointmentHistoryResponseModel.projectName}", + scheduleDateTime: DateUtil.convertStringToDate(widget + .patientAppointmentHistoryResponseModel.appointmentDate), + eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", + location: '', + reminderMinutes: selectedIndex + ); + setState(() { + myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + }); + }, + ); + }else { + isEventAddedOrRemoved = await calender.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + setState(() { + myAppointmentsViewModel.setAppointmentReminder(!isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + }); + } + + }, ), ], @@ -234,219 +349,347 @@ class _AppointmentDetailsPageState extends State { SizedBox(height: 16.h), ], ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + // : SizedBox.shrink() + : GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16.h, + mainAxisSpacing: 16.w, + mainAxisExtent: 115.h, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, children: [ - "Lab & Radiology".needTranslation.toText18(isBold: true), - SizedBox(height: 16.h), - GridView( - padding: EdgeInsets.zero, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet || isFoldable ? 3 : 2, - crossAxisSpacing: 13.w, - mainAxisSpacing: 13.w, - ), - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - children: [ - MedicalFileCard( - label: LocaleKeys.labResults.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.lab_result_icon, - iconSize: 40.w, - isLargeText: true, - ), - MedicalFileCard( - label: "Radiology Results".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.radiology_icon, - iconSize: 40.w, - isLargeText: true, - ), - MedicalFileCard( - label: LocaleKeys.labResults.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.lab_result_icon, - iconSize: 40.w, - isLargeText: true, - ), - MedicalFileCard( - label: "Radiology Results".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.radiology_icon, - iconSize: 40.w, - isLargeText: true, - ), - ], - ), - SizedBox(height: 16.h), - LocaleKeys.prescriptions.tr().toText18(isBold: true), - SizedBox(height: 16.h), - Consumer(builder: (context, prescriptionVM, child) { - return prescriptionVM.isPrescriptionsDetailsLoading - ? const MoviesShimmerWidget() - : Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, - borderRadius: 20.r, - ), - padding: EdgeInsets.all(16.w), - child: Column( - children: [ - ListView.separated( - itemCount: prescriptionVM.prescriptionDetailsList.length, - shrinkWrap: true, - padding: EdgeInsets.only(right: 8.w), - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.prescription_item_icon, - width: 40.h, - height: 40.h, - ), - SizedBox(width: 8.h), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Column( - children: [ - prescriptionVM.prescriptionDetailsList[index].itemDescription! - .toText12(isBold: true, maxLine: 1), - "Prescribed By: ${widget.patientAppointmentHistoryResponseModel.doctorTitle} ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}" - .needTranslation - .toText10( - weight: FontWeight.w500, - color: AppColors.greyTextColor, - letterSpacing: -0.4), - ], - ), - SizedBox(width: 68.w), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.blackColor, - width: 18.w, - height: 13.h, - fit: BoxFit.contain, - ), - ), - ], - ), - ], - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ).onPress(() { - prescriptionVM.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage(prescriptionsResponseModel: getPrescriptionRequestModel()), - ), - ); - }), - SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor), - SizedBox(height: 16.h), - Wrap( - runSpacing: 6.w, - children: [ - // Expanded( - // child: CustomButton( - // text: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? LocaleKeys.resendOrder.tr(context: context) : LocaleKeys.prescriptionDeliveryError.tr(context: context), - // onPressed: () {}, - // backgroundColor: AppColors.secondaryLightRedColor, - // borderColor: AppColors.secondaryLightRedColor, - // textColor: AppColors.primaryRedColor, - // fontSize: 14, - // fontWeight: FontWeight.w500, - // borderRadius: 12.h, - // height: 40.h, - // icon: AppAssets.appointment_calendar_icon, - // iconColor: AppColors.primaryRedColor, - // iconSize: 16.h, - // ), - // ), - // SizedBox(width: 16.h), - Expanded( - child: CustomButton( - text: "Refill & Delivery".needTranslation, - onPressed: () { - Navigator.of(context) - .push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ) - .then((val) { - prescriptionsViewModel.setPrescriptionsDetailsLoading(); - prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel()); - }); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 40.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - ), - ), - - SizedBox(width: 16.w), - Expanded( - child: CustomButton( - text: "All Prescriptions".needTranslation, - onPressed: () { - Navigator.of(context) - .push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ) - .then((val) { - prescriptionsViewModel.setPrescriptionsDetailsLoading(); - prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel()); - }); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 40.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - ), - ), - ], - ), - ], + MedicalFileCard( + label: LocaleKeys.labResults.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.lab_result_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Lab Results...".needTranslation); + await labViewModel.getLabResultsByAppointmentNo( + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj!, + clinicName: widget.patientAppointmentHistoryResponseModel.clinicName!, + projectName: widget.patientAppointmentHistoryResponseModel.projectName!, + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + }), + MedicalFileCard( + label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}", + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.radiology_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Radiology Results...".needTranslation); + await radiologyViewModel.getPatientRadiologyOrdersByAppointment( + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + }), + MedicalFileCard( + label: LocaleKeys.prescriptions.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.prescription_item_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Appointment Prescriptions...".needTranslation); + await prescriptionsViewModel.getPrescriptionDetails( + getPrescriptionRequestModel(), + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + if (val.data.isNotEmpty) { + PatientPrescriptionsResponseModel patientPrescriptionsResponseModel = PatientPrescriptionsResponseModel( + doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, + doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj, + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate, + clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName, + decimalDoctorRate: widget.patientAppointmentHistoryResponseModel.decimalDoctorRate, + name: widget.patientAppointmentHistoryResponseModel.projectName, + isHomeMedicineDeliverySupported: false, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + setupID: widget.patientAppointmentHistoryResponseModel.setupID, + ); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage( + isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel), ), ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget( + loadingText: "You don't have any prescriptions for this appointment.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + // Navigator.of(context).push( + // CustomPageRoute( + // page: VaccineListPage(), + // ), + // ); }), ], ), + // Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // "Lab & Radiology".needTranslation.toText18(isBold: true), + // SizedBox(height: 16.h), + // Row( + // children: [ + // Expanded( + // child: LabRadCard( + // icon: AppAssets.lab_result_icon, + // labelText: LocaleKeys.labResults.tr(context: context), + // // labOrderTests: ["Complete blood count", "Creatinine", "Blood Sugar"], + // // labOrderTests: labViewModel.isLabOrdersLoading ? [] : labViewModel.labOrderTests, + // labOrderTests: [], + // // isLoading: labViewModel.isLabOrdersLoading, + // isLoading: false, + // ).onPress(() { + // Navigator.of(context).push( + // CustomPageRoute( + // page: LabOrdersPage(), + // ), + // ); + // }), + // ), + // SizedBox(width: 16.h), + // Expanded( + // child: LabRadCard( + // icon: AppAssets.radiology_icon, + // labelText: LocaleKeys.radiology.tr(context: context), + // // labOrderTests: ["Chest X-ray", "Abdominal Ultrasound", "Dental X-ray"], + // labOrderTests: [], + // isLoading: false, + // ).onPress(() { + // Navigator.of(context).push( + // CustomPageRoute( + // page: RadiologyOrdersPage(), + // ), + // ); + // }), + // ), + // ], + // ), + // SizedBox(height: 16.h), + // LocaleKeys.prescriptions.tr().toText18(isBold: true), + // SizedBox(height: 16.h), + // Consumer(builder: (context, prescriptionVM, child) { + // return prescriptionVM.isPrescriptionsDetailsLoading + // ? const MoviesShimmerWidget() + // : Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + // color: Colors.white, + // borderRadius: 20.r, + // ), + // padding: EdgeInsets.all(16.w), + // child: Column( + // children: [ + // // ListView.separated( + // // itemCount: prescriptionVM.prescriptionDetailsList.length, + // // shrinkWrap: true, + // // padding: EdgeInsets.only(right: 8.w), + // // physics: NeverScrollableScrollPhysics(), + // // itemBuilder: (context, index) { + // // return AnimationConfiguration.staggeredList( + // // position: index, + // // duration: const Duration(milliseconds: 500), + // // child: SlideAnimation( + // // verticalOffset: 100.0, + // // child: FadeInAnimation( + // // child: Row( + // // children: [ + // // Utils.buildSvgWithAssets( + // // icon: AppAssets.prescription_item_icon, + // // width: 40.h, + // // height: 40.h, + // // ), + // // SizedBox(width: 8.h), + // // Row( + // // mainAxisSize: MainAxisSize.max, + // // children: [ + // // Column( + // // children: [ + // // prescriptionVM.prescriptionDetailsList[index].itemDescription! + // // .toText12(isBold: true, maxLine: 1), + // // "Prescribed By: ${widget.patientAppointmentHistoryResponseModel.doctorTitle} ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}" + // // .needTranslation + // // .toText10( + // // weight: FontWeight.w500, + // // color: AppColors.greyTextColor, + // // letterSpacing: -0.4), + // // ], + // // ), + // // SizedBox(width: 68.w), + // // Transform.flip( + // // flipX: appState.isArabic(), + // // child: Utils.buildSvgWithAssets( + // // icon: AppAssets.forward_arrow_icon, + // // iconColor: AppColors.blackColor, + // // width: 18.w, + // // height: 13.h, + // // fit: BoxFit.contain, + // // ), + // // ), + // // ], + // // ), + // // ], + // // ), + // // ), + // // ), + // // ); + // // }, + // // separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + // // ).onPress(() { + // // prescriptionVM.setPrescriptionsDetailsLoading(); + // // Navigator.of(context).push( + // // CustomPageRoute( + // // page: PrescriptionDetailPage(prescriptionsResponseModel: getPrescriptionRequestModel()), + // // ), + // // ); + // // }), + // SizedBox(height: 16.h), + // const Divider(color: AppColors.dividerColor), + // SizedBox(height: 16.h), + // // Wrap( + // // runSpacing: 6.w, + // // children: [ + // // // Expanded( + // // // child: CustomButton( + // // // text: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? LocaleKeys.resendOrder.tr(context: context) : LocaleKeys.prescriptionDeliveryError.tr(context: context), + // // // onPressed: () {}, + // // // backgroundColor: AppColors.secondaryLightRedColor, + // // // borderColor: AppColors.secondaryLightRedColor, + // // // textColor: AppColors.primaryRedColor, + // // // fontSize: 14, + // // // fontWeight: FontWeight.w500, + // // // borderRadius: 12.h, + // // // height: 40.h, + // // // icon: AppAssets.appointment_calendar_icon, + // // // iconColor: AppColors.primaryRedColor, + // // // iconSize: 16.h, + // // // ), + // // // ), + // // // SizedBox(width: 16.h), + // // Expanded( + // // child: CustomButton( + // // text: "Refill & Delivery".needTranslation, + // // onPressed: () { + // // Navigator.of(context) + // // .push( + // // CustomPageRoute( + // // page: PrescriptionsListPage(), + // // ), + // // ) + // // .then((val) { + // // prescriptionsViewModel.setPrescriptionsDetailsLoading(); + // // prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel()); + // // }); + // // }, + // // backgroundColor: AppColors.secondaryLightRedColor, + // // borderColor: AppColors.secondaryLightRedColor, + // // textColor: AppColors.primaryRedColor, + // // fontSize: 14.f, + // // fontWeight: FontWeight.w500, + // // borderRadius: 12.r, + // // height: 40.h, + // // icon: AppAssets.requests, + // // iconColor: AppColors.primaryRedColor, + // // iconSize: 16.h, + // // ), + // // ), + // // + // // SizedBox(width: 16.w), + // // Expanded( + // // child: CustomButton( + // // text: "All Prescriptions".needTranslation, + // // onPressed: () { + // // Navigator.of(context) + // // .push( + // // CustomPageRoute( + // // page: PrescriptionsListPage(), + // // ), + // // ) + // // .then((val) { + // // prescriptionsViewModel.setPrescriptionsDetailsLoading(); + // // prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel()); + // // }); + // // }, + // // backgroundColor: AppColors.secondaryLightRedColor, + // // borderColor: AppColors.secondaryLightRedColor, + // // textColor: AppColors.primaryRedColor, + // // fontSize: 14.f, + // // fontWeight: FontWeight.w500, + // // borderRadius: 12.r, + // // height: 40.h, + // // icon: AppAssets.requests, + // // iconColor: AppColors.primaryRedColor, + // // iconSize: 16.h, + // // ), + // // ), + // // ], + // // ), + // ], + // ), + // ); + // }), + // ], + // ), ], ).paddingAll(24.w), ), @@ -519,8 +762,8 @@ class _AppointmentDetailsPageState extends State { onPressed: () { openDoctorScheduleCalendar(); }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, textColor: AppColors.whiteColor, fontSize: 16.f, fontWeight: FontWeight.w500, diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 467a480..475ee70 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -9,21 +9,19 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/services/cache_service.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'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; @@ -59,19 +57,20 @@ class _AppointmentPaymentPageState extends State { void initState() { scheduleMicrotask(() { payfortViewModel.initPayfortViewModel(); - myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { - if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && - myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { - setState(() { - isShowTamara = true; - }); - } - }); payfortViewModel.setIsApplePayConfigurationLoading(false); myAppointmentsViewModel.getPatientShareAppointment( widget.patientAppointmentHistoryResponseModel.projectID, widget.patientAppointmentHistoryResponseModel.clinicID, - widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onError: (err) { + widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) { + myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { + if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + setState(() { + isShowTamara = true; + }); + } + }); + }, onError: (err) { Navigator.of(context).pop(); Navigator.of(context).pop(); }); @@ -109,7 +108,8 @@ class _AppointmentPaymentPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), + Image.asset(AppAssets.mada, width: 72.h, height: 25.h) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), SizedBox(height: 16.h), "Mada".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), ], @@ -149,11 +149,14 @@ class _AppointmentPaymentPageState extends State { children: [ Image.asset(AppAssets.visa, width: 50.h, height: 50.h), SizedBox(width: 8.h), - Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), ], ).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), SizedBox(height: 16.h), - "Visa or Mastercard".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), + "Visa or Mastercard" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), ], ), SizedBox(width: 8.h), @@ -181,23 +184,27 @@ class _AppointmentPaymentPageState extends State { color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), - SizedBox(height: 16.h), - "Tamara".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), - ], - ), - SizedBox(width: 8.h), - const Spacer(), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), + SizedBox(height: 16.h), + "Tamara" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, @@ -243,7 +250,10 @@ class _AppointmentPaymentPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Insurance expired or inactive".needTranslation.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500).paddingSymmetrical(24.h, 0.h), + "Insurance expired or inactive" + .needTranslation + .toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500) + .paddingSymmetrical(24.h, 0.h), CustomButton( text: LocaleKeys.updateInsurance.tr(context: context), onPressed: () { @@ -256,7 +266,7 @@ class _AppointmentPaymentPageState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.secondaryLightRedBorderColor, textColor: AppColors.whiteColor, - fontSize: 10, + fontSize: 10.f, fontWeight: FontWeight.w500, borderRadius: 8, padding: EdgeInsets.fromLTRB(15, 0, 15, 0), @@ -273,7 +283,10 @@ class _AppointmentPaymentPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "Amount before tax".needTranslation.toText14(isBold: true), - Utils.getPaymentAmountWithSymbol(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13, + Utils.getPaymentAmountWithSymbol( + myAppointmentsVM.patientAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true), + AppColors.blackColor, + 13, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), @@ -282,7 +295,9 @@ class _AppointmentPaymentPageState extends State { children: [ "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), Utils.getPaymentAmountWithSymbol( - myAppointmentsVM.patientAppointmentShareResponseModel!.patientTaxAmount!.toString().toText14(isBold: true, color: AppColors.greyTextColor), + myAppointmentsVM.patientAppointmentShareResponseModel!.patientTaxAmount! + .toString() + .toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, isSaudiCurrency: true), @@ -293,7 +308,10 @@ class _AppointmentPaymentPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "".needTranslation.toText14(isBold: true), - Utils.getPaymentAmountWithSymbol(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShareWithTax!.toString().toText24(isBold: true), AppColors.blackColor, 17, + Utils.getPaymentAmountWithSymbol( + myAppointmentsVM.patientAppointmentShareResponseModel!.patientShareWithTax!.toString().toText24(isBold: true), + AppColors.blackColor, + 17, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), @@ -372,16 +390,17 @@ class _AppointmentPaymentPageState extends State { onSuccess: (apiResponse) async { if (apiResponse.data["status"].toString().toLowerCase() == "success") { tamaraOrderID = apiResponse.data["tamara_order_id"].toString(); - await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + await payfortViewModel.updateTamaraRequestStatus( + responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); await payfortViewModel.markAppointmentAsTamaraPaid( - projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); await myAppointmentsViewModel.addAdvanceNumberRequest( advanceNumber: "Tamara-Advance-0000", paymentReference: tamaraOrderID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), onSuccess: (value) async { if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { - //TODO: Implement LiveCare Check-In API Call await myAppointmentsViewModel.insertLiveCareVIDARequest( clientRequestID: tamaraOrderID, patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, @@ -417,7 +436,8 @@ class _AppointmentPaymentPageState extends State { } }); } else { - await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + await payfortViewModel.updateTamaraRequestStatus( + responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, @@ -535,7 +555,9 @@ class _AppointmentPaymentPageState extends State { browser!, widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, "2", - widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? myAppointmentsViewModel.patientAppointmentShareResponseModel!.clinicID.toString() : "", + widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! + ? myAppointmentsViewModel.patientAppointmentShareResponseModel!.clinicID.toString() + : "", context, myAppointmentsViewModel.patientAppointmentShareResponseModel!.appointmentDate, myAppointmentsViewModel.patientAppointmentShareResponseModel!.appointmentNo, @@ -546,7 +568,13 @@ class _AppointmentPaymentPageState extends State { startApplePay() async { showCommonBottomSheet(context, - child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false); + child: Utils.getLoadingWidget(), + callBackFunc: (str) {}, + title: "", + height: ResponsiveExtension.screenHeight * 0.3, + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false); transID = Utils.getAppointmentTransID( widget.patientAppointmentHistoryResponseModel.projectID, widget.patientAppointmentHistoryResponseModel.clinicID, @@ -556,7 +584,9 @@ class _AppointmentPaymentPageState extends State { ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); await payfortViewModel.getPayfortConfigurations( - serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 2); + serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), + projectId: widget.patientAppointmentHistoryResponseModel.projectID, + integrationId: 2); applePayInsertRequest.clientRequestID = transID; applePayInsertRequest.clinicID = widget.patientAppointmentHistoryResponseModel.clinicID; @@ -625,7 +655,6 @@ class _AppointmentPaymentPageState extends State { ); }, onSucceeded: (successResult) async { - Navigator.of(context).pop(); log("successResult: ${successResult.responseMessage.toString()}"); selectedPaymentMethod = successResult.paymentOption ?? "VISA"; checkPaymentStatus(); diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart new file mode 100644 index 0000000..124bf25 --- /dev/null +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class AppointmentQueuePage extends StatelessWidget { + AppointmentQueuePage({super.key}); + + AppState? appState; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, myAppointmentsVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Queueing".needTranslation, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + side: BorderSide( + color: myAppointmentsVM.isAppointmentQueueDetailsLoading + ? AppColors.whiteColor + : Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + width: 2.w), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: myAppointmentsVM.currentQueueStatus == 0 ? "In Queue".needTranslation : "Your Turn".needTranslation, + backgroundColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.20), + textColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + ], + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 10.h), + "Hala ${appState!.getAuthenticatedUser()!.firstName}!!!" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 8.h), + "Thank you for your patience, here is your queue number." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 8.h), + myAppointmentsVM.currentPatientQueueDetails.queueNo! + .toText32(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 8.h), + CustomButton( + text: Utils.getCardButtonText( + myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), + onPressed: () {}, + backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), + borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), + textColor: Utils.getCardButtonTextColor(myAppointmentsVM.currentQueueStatus), + fontSize: 12.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + ], + ), + ), + ), + SizedBox(height: 16.h), + myAppointmentsVM.patientQueueDetailsList.isNotEmpty + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Serving Now" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 18.h), + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: myAppointmentsVM.patientQueueDetailsList.length, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + myAppointmentsVM.patientQueueDetailsList[index].queueNo!.toText17(isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + "Room: ${myAppointmentsVM.patientQueueDetailsList[index].roomNo}" + .toText12(fontWeight: FontWeight.w500), + SizedBox(width: 8.w), + AppCustomChipWidget( + deleteIcon: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppAssets.call_for_vitals + : AppAssets.call_for_doctor, + labelText: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? "Call for vital signs".needTranslation + : "Call for Doctor".needTranslation, + iconColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor + : AppColors.successColor, + textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor + : AppColors.successColor, + iconSize: 14.w, + backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor.withValues(alpha: 0.1) + : AppColors.successColor.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), + ), + ], + ), + ], + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + ], + ), + ), + ) + : SizedBox.shrink(), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.4, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bulb_icon, width: 24.w, height: 24.h), + SizedBox(width: 8.w), + "Things to ask your doctor today".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(height: 8.h), + + // What can I do to improve my overall health? + // Are there any routine screenings I should get? + // What is this medication for? + // Are there any side effects I should know about? + // When should I come back for a follow-up? + + "• ${"What can I do to improve my overall health?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 4.h), + "• ${"Are there any routine screenings I should get?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 4.h), + "• ${"What is this medication for?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 4.h), + "• ${"Are there any side effects I should know about?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 4.h), + "• ${"When should I come back for a follow-up?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + + SizedBox(height: 16.h), + ], + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + ), + ), + ], + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: CustomButton( + text: "Go to homepage".needTranslation, + onPressed: () { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 50.h, + icon: AppAssets.homeBottom, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(16.h, 24.h), + ) + ], + ); + }), + ); + } +} diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 7481bf2..b4c3630 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -19,6 +19,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointme 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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; @@ -35,6 +36,7 @@ class MyAppointmentsPage extends StatefulWidget { } class _MyAppointmentsPageState extends State { + int? expandedIndex; late MyAppointmentsViewModel myAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; @@ -68,6 +70,9 @@ class _MyAppointmentsPageState extends State { CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { + setState(() { + expandedIndex = null; + }); myAppointmentsViewModel.onTabChange(index); myAppointmentsViewModel.updateListWRTTab(index); context.read().flush(); @@ -91,17 +96,55 @@ class _MyAppointmentsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 16.h), + // Clinic & Hospital Sort + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + myAppointmentsVM.setIsAppointmentsSortByClinic(true); + }, + backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + myAppointmentsVM.setIsAppointmentsSortByClinic(false); + }, + backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), Visibility(visible: myAppointmentsVM.availableFilters.isNotEmpty, child: getAppointmentFilters(myAppointmentsVM)), - ListView.separated( - padding: EdgeInsets.only(top: 24.h), + SizedBox(height: 8.h), + // Expandable list + ListView.builder( + padding: EdgeInsets.only(top: 8.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 5 + ? 4 : filteredAppointmentList.isNotEmpty - ? filteredAppointmentList.length + ? myAppointmentsVM.patientAppointmentsViewList.length : 1, itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), @@ -123,15 +166,83 @@ class _MyAppointmentsPageState extends State { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: filteredAppointmentList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: false, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: + "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + myAppointmentsVM.patientAppointmentsViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.map((appointment) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppointmentCard( + patientAppointmentHistoryResponseModel: appointment, + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: false, + ), + SizedBox(height: 8.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h).paddingSymmetrical(16.w, 0.h), + SizedBox(height: 8.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), ), - ).paddingSymmetrical(24.h, 0.h), + ), ), ), ) @@ -160,8 +271,7 @@ class _MyAppointmentsPageState extends State { ).paddingSymmetrical(48.h, 0.h), ); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 24.h), ], ); @@ -169,6 +279,7 @@ class _MyAppointmentsPageState extends State { Widget getAppointmentFilters(MyAppointmentsViewModel myAppointmentsVM) { return SizedBox( + height: 60.h, child: Row( children: [ Expanded( @@ -177,6 +288,7 @@ class _MyAppointmentsPageState extends State { width: 8.h, ), scrollDirection: Axis.horizontal, + shrinkWrap: true, itemCount: myAppointmentsVM.availableFilters.length, itemBuilder: (_, index) => AppointmentFilters( selectedFilter: myAppointmentsVM.selectedFilter, @@ -205,6 +317,6 @@ class _MyAppointmentsPageState extends State { )), ), ], - )).paddingOnly(top: 24.h, left: 24.h, right: 24.h); + )).paddingOnly(top: 16.h, left: 24.h, right: 24.h); } } diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 51e9703..2c5d1b0 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -1,9 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -21,169 +18,325 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; +import '../../widgets/buttons/custom_button.dart'; -class MyDoctorsPage extends StatelessWidget { +class MyDoctorsPage extends StatefulWidget { MyDoctorsPage({super.key}); + @override + State createState() => _MyDoctorsPageState(); +} + +class _MyDoctorsPageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + bool isSortByClinic = true; // default like lab page + int? expandedIndex; + final Map _groupKeys = {}; + @override Widget build(BuildContext context) { - AppState appState = getIt.get(); myAppointmentsViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.myDoctor.tr(context: context), - child: SingleChildScrollView( - child: Consumer(builder: (context, myAppointmentsVM, child) { + + return CollapsingListView( + title: LocaleKeys.myDoctor.tr(context: context), + child: Consumer(builder: (context, myAppointmentsVM, child) { + // build grouped lists from the flat list + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doc in myAppointmentsVM.patientMyDoctorsList) { + final clinicKey = (doc.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doc); + + final hospitalKey = (doc.projectName ?? doc.projectID?.toString() ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doc); + } + + final patientMyDoctorsByClinic = clinicMap.values.toList(); + final patientMyDoctorsByHospital = hospitalMap.values.toList(); + final patientMyDoctorsViewList = isSortByClinic ? patientMyDoctorsByClinic : patientMyDoctorsByHospital; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), + // Add Clinic / Hospital toggle buttons styled like LabOrdersPage + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + setState(() { + isSortByClinic = true; + expandedIndex = null; + }); + }, + backgroundColor: isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + + + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + setState(() { + isSortByClinic = false; + expandedIndex = null; + }); + }, + backgroundColor: isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + + ), + ], + ), + ), + SizedBox(height: 16.h), ListView.separated( scrollDirection: Axis.vertical, - itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length, + itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : (patientMyDoctorsViewList.isNotEmpty ? patientMyDoctorsViewList.length : 0), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.only(left: 24.h, right: 24.h), itemBuilder: (context, index) { - return myAppointmentsVM.isPatientMyDoctorsLoading - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( + if (myAppointmentsVM.isPatientMyDoctorsLoading) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.network( - "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", - width: 63.h, - height: 63.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: true), - SizedBox(width: 16.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), - AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), - ], - ), - ], - ), - ), - ], - ), - ], - ), - ), - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true), + SizedBox(width: 16.h), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 63.h, - height: 63.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false), - SizedBox(width: 16.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (myAppointmentsVM.patientMyDoctorsList[index].doctorName).toString().toText16(isBold: true).toShimmer2(isShow: false), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].clinicName).toShimmer2(isShow: false, width: 16.h), - AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].projectName).toShimmer2(isShow: false, width: 16.h), - ], - ), - ], - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "".toText16(), - Transform.flip( - flipX: appState.isArabic(), - child: - Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), ], ), ], ), ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + ], ), + ], + ), + ), + ); + } + + final group = patientMyDoctorsViewList[index]; + final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); + final isExpanded = expandedIndex == index; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.0, + ); + } + }); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget(labelText: "${group.length} ${'doctors'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((doctor) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"), + width: 24.w, + height: 24.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Expanded( + child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), + ), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + flex: 2, + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: "View Profile".needTranslation.tr(context: context), + onPressed: () async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: doctor?.clinicID ?? 0, + projectID: doctor?.projectID ?? 0, + doctorID: doctor?.doctorID ?? 0, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), ), - ); + ], + ), + ), + ); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), @@ -191,8 +344,6 @@ class MyDoctorsPage extends StatelessWidget { ], ); }), - ), - ), ); } } diff --git a/lib/presentation/appointments/widgets/AppointmentFilter.dart b/lib/presentation/appointments/widgets/AppointmentFilter.dart index 98da22d..6546c65 100644 --- a/lib/presentation/appointments/widgets/AppointmentFilter.dart +++ b/lib/presentation/appointments/widgets/AppointmentFilter.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:smooth_corner/smooth_corner.dart'; diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 627d527..39e4e03 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -10,13 +10,16 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; +import 'package:hmg_patient_app_new/presentation/medical_file/eye_measurement_details_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -30,9 +33,12 @@ class AppointmentCard extends StatelessWidget { final bool isLoading; final bool isFromHomePage; final bool isFromMedicalReport; + final bool isForEyeMeasurements; + final bool isForFeedback; final MedicalFileViewModel? medicalFileViewModel; + final ContactUsViewModel? contactUsViewModel; final BookAppointmentsViewModel bookAppointmentsViewModel; - + final bool isForRate; const AppointmentCard({ super.key, required this.patientAppointmentHistoryResponseModel, @@ -41,7 +47,11 @@ class AppointmentCard extends StatelessWidget { this.isLoading = false, this.isFromHomePage = false, this.isFromMedicalReport = false, + this.isForEyeMeasurements = false, + this.isForFeedback = false, this.medicalFileViewModel, + this.contactUsViewModel, + this.isForRate =false }); @override @@ -54,11 +64,11 @@ class AppointmentCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(context, appState), + isForRate ? SizedBox(): _buildHeader(context, appState), SizedBox(height: 16.h), _buildDoctorRow(context), SizedBox(height: 16.h), - _buildActionArea(context, appState), + isForRate ? SizedBox(): _buildActionArea(context, appState), ], ), ), @@ -114,22 +124,46 @@ class AppointmentCard extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.network( - isLoading - ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' - : patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.h, - height: 63.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: isLoading), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r).toShimmer2(isShow: isLoading), + Transform.translate( + offset: Offset(0.0, -20.h), + child: Container( + width: 40.w, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.circle, // Makes the container circular + border: Border.all( + color: AppColors.scaffoldBgColor, // Color of the border + width: 1.5.w, // Width of the border + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + SizedBox(height: 2.h), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + ], + ), + ).circle(100).toShimmer2(isShow: isLoading), + ), + ], + ), SizedBox(width: 16.h), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (isLoading - ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' - : patientAppointmentHistoryResponseModel.doctorNameObj!) + (isLoading ? 'John Doe' : "${patientAppointmentHistoryResponseModel.doctorTitle} ${patientAppointmentHistoryResponseModel.doctorNameObj!}") .toText16(isBold: true, maxlines: 1) .toShimmer2(isShow: isLoading), SizedBox(height: 8.h), @@ -138,26 +172,48 @@ class AppointmentCard extends StatelessWidget { spacing: 3.h, runSpacing: 4.h, children: [ - if (!isFromHomePage) - AppCustomChipWidget(labelText: isLoading ? 'Cardiology' : patientAppointmentHistoryResponseModel.clinicName!) - .toShimmer2(isShow: isLoading), - if (!isFromHomePage) - AppCustomChipWidget(labelText: isLoading ? 'Olaya' : patientAppointmentHistoryResponseModel.projectName!) - .toShimmer2(isShow: isLoading), AppCustomChipWidget( + labelText: isLoading + ? 'Cardiology' + : (patientAppointmentHistoryResponseModel.clinicName!.length > 15 + ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 12)}...' + : patientAppointmentHistoryResponseModel.clinicName!), + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelText: isLoading + ? 'Olaya' + : patientAppointmentHistoryResponseModel.projectName!.length > 15 + ? '${patientAppointmentHistoryResponseModel.projectName!.substring(0, 12)}...' + : patientAppointmentHistoryResponseModel.projectName!) + .toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.appointment_calendar_icon, labelText: isLoading ? 'Cardiology' - : DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false), + : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}", ).toShimmer2(isShow: isLoading), - if (!isFromMedicalReport) - AppCustomChipWidget( - icon: AppAssets.appointment_time_icon, - labelText: isLoading - ? 'Cardiology' - : DateUtil.formatDateToTimeLang( - DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false), - ).toShimmer2(isShow: isLoading), + + // AppCustomChipWidget( + // labelPadding: EdgeInsetsDirectional.only(start: -2.w, end: 6.w), + // isIconPNG: true, + // icon: getIt.get().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, + // iconSize: 18.h, + // labelText: isLoading ? 'Cardiology' : "Patient: ${getIt.get().getAuthenticatedUser()!.firstName!}", + // ).toShimmer2(isShow: isLoading), + // if (!isFromMedicalReport) + // AppCustomChipWidget( + // icon: AppAssets.appointment_time_icon, + // labelText: isLoading + // ? 'Cardiology' + // : DateUtil.formatDateToTimeLang( + // DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false), + // ).toShimmer2(isShow: isLoading), + // AppCustomChipWidget( + // icon: AppAssets.rating_icon, + // iconColor: AppColors.ratingColorYellow, + // labelText: isLoading ? "Rating" : "Rating: ${patientAppointmentHistoryResponseModel.decimalDoctorRate}".needTranslation) + // .toShimmer2(isShow: isLoading), ], ), ], @@ -169,24 +225,33 @@ class AppointmentCard extends StatelessWidget { Widget _buildActionArea(BuildContext context, AppState appState) { if (isFromMedicalReport) { - return CustomButton( - text: 'Select appointment'.needTranslation, - onPressed: () { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); - Navigator.pop(context, false); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, - icon: AppAssets.checkmark_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - ); + if (isForEyeMeasurements) { + return SizedBox.shrink(); + } else { + return CustomButton( + text: 'Select appointment'.needTranslation, + onPressed: () { + if (isForFeedback) { + contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + } else { + medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + } + Navigator.pop(context, false); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ); + } } return Row( @@ -207,7 +272,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), iconSize: 15.h, @@ -244,7 +309,34 @@ class AppointmentCard extends StatelessWidget { if (isRecent) { return CustomButton( text: LocaleKeys.askDoctor.tr(context: context), - onPressed: () {}, + onPressed: () async { + LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); + await myAppointmentsViewModel.isDoctorAvailable( + projectID: patientAppointmentHistoryResponseModel.projectID, + doctorId: patientAppointmentHistoryResponseModel.doctorID, + clinicId: patientAppointmentHistoryResponseModel.clinicID, + onSuccess: (value) async { + if (value) { + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + print("Doctor is not available"); + } + }); + }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, @@ -252,7 +344,8 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.ask_doctor_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, @@ -269,7 +362,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, iconSize: 16.h, @@ -277,16 +370,26 @@ class AppointmentCard extends StatelessWidget { } void _goToDetails(BuildContext context) { - Navigator.of(context) - .push( - CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), - ), - ) - .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); - }); + if (isFromMedicalReport) return; + if (isForEyeMeasurements) { + Navigator.of(context).push( + CustomPageRoute( + page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + ), + ); + } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID); + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + ), + ) + .then((_) { + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); + }); + } } void openDoctorScheduleCalendar(BuildContext context) async { diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index 1863875..d118a5e 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -13,14 +13,14 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_queue_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; class AppointmentCheckinBottomSheet extends StatelessWidget { AppointmentCheckinBottomSheet({super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel}); @@ -58,7 +58,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { double dist = Utils.distance(value.latitude, value.longitude, double.parse(projectDetailListModel.latitude!), double.parse(projectDetailListModel.longitude!)).ceilToDouble() * 1000; print(dist); if (dist <= projectDetailListModel.geofenceRadius!) { - sendCheckInRequest(projectDetailListModel.checkInQrCode!, context); + sendCheckInRequest(projectDetailListModel.checkInQrCode!, 3, context); } else { showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, @@ -77,7 +77,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { Future.delayed(const Duration(milliseconds: 500), () { showNfcReader(context, onNcfScan: (String nfcId) { Future.delayed(const Duration(milliseconds: 100), () { - sendCheckInRequest(nfcId, context); + sendCheckInRequest(nfcId, 1, context); }); }, onCancel: () {}); }); @@ -90,7 +90,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ).onPress(() async { String onlineCheckInQRCode = (await BarcodeScanner.scan().then((value) => value.rawContent)); if (onlineCheckInQRCode != "") { - sendCheckInRequest(onlineCheckInQRCode, context); + sendCheckInRequest(onlineCheckInQRCode, 2, context); } else {} }), ], @@ -138,16 +138,16 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ); } - void sendCheckInRequest(String scannedCode, BuildContext context) async { - showCommonBottomSheet(context, - child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false); + void sendCheckInRequest(String scannedCode, int checkInType, BuildContext context) async { + LoaderBottomSheet.showLoader(loadingText: "Processing Check-In...".needTranslation); await myAppointmentsViewModel.sendCheckInNfcRequest( patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, scannedCode: scannedCode, - checkInType: 2, + checkInType: checkInType, onSuccess: (apiResponse) { - Navigator.of(context).pop(); - showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () async { + await myAppointmentsViewModel.getPatientAppointmentQueueDetails(); Navigator.of(context).pop(); Navigator.pushAndRemoveUntil( context, @@ -156,12 +156,14 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ), (r) => false); Navigator.of(context).push( - CustomPageRoute(page: MyAppointmentsPage()), + CustomPageRoute( + page: AppointmentQueuePage(), + ), ); }, isFullScreen: false); }, onError: (error) { - Navigator.of(context).pop(); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: error), callBackFunc: () { Navigator.of(context).pop(); }, isFullScreen: false); diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 0b75977..ccf6674 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; @@ -42,25 +43,55 @@ class AppointmentDoctorCard extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.network( - patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.w, - height: 63.h, - fit: BoxFit.cover, - ).circle(100.r), + Column( + children: [ + Image.network( + patientAppointmentHistoryResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + Transform.translate( + offset: Offset(0.0, -20.h), + child: Container( + width: 40.w, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.circle, // Makes the container circular + border: Border.all( + color: AppColors.scaffoldBgColor, // Color of the border + width: 1.5.w, // Width of the border + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + SizedBox(height: 2.h), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + ], + ), + ).circle(100), + ), + ], + ), SizedBox(width: 16.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(isBold: true), + SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 6.w, runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: patientAppointmentHistoryResponseModel.clinicName!, + labelText: (patientAppointmentHistoryResponseModel.clinicName!.length > 15 + ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 12)}...' + : patientAppointmentHistoryResponseModel.clinicName!), labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), ), AppCustomChipWidget( @@ -68,14 +99,15 @@ class AppointmentDoctorCard extends StatelessWidget { labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), ), AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - labelText: - "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}, ${DateUtil.formatDateToTimeLang( + labelText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang( DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false, )}", ), AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, @@ -87,10 +119,6 @@ class AppointmentDoctorCard extends StatelessWidget { !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor, textColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor, ), - AppCustomChipWidget( - icon: AppAssets.rating_icon, - iconColor: AppColors.ratingColorYellow, - labelText: "Rating: ${patientAppointmentHistoryResponseModel.decimalDoctorRate}"), ], ), ], @@ -98,8 +126,7 @@ class AppointmentDoctorCard extends StatelessWidget { ), ], ), - SizedBox(height: 16.h), - + SizedBox(height: 8.h), Visibility( visible: renderWidgetForERDisplay == false, child: getAppointmentActionButtons( @@ -117,7 +144,9 @@ class AppointmentDoctorCard extends StatelessWidget { return DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15 ? CustomButton( text: LocaleKeys.askDoctor.tr(), - onPressed: () {}, + onPressed: () { + onAskDoctorTap(); + }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, @@ -138,14 +167,14 @@ class AppointmentDoctorCard extends StatelessWidget { backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, - fontSize: 14.f, + fontSize: 12.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, - iconSize: 16.h, + iconSize: 14.h, ); } else { return patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false diff --git a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart new file mode 100644 index 0000000..01aab4e --- /dev/null +++ b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart @@ -0,0 +1,138 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; + +class AskDoctorRequestTypeSelect extends StatelessWidget { + AskDoctorRequestTypeSelect({super.key, required this.askDoctorRequestTypeList, required this.myAppointmentsViewModel, required this.patientAppointmentHistoryResponseModel}); + + final MyAppointmentsViewModel myAppointmentsViewModel; + final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; + List askDoctorRequestTypeList = []; + int selectedParameterCodeValue = 2; + int selectedParameterCode = 0; + + final ValueNotifier requestTypeSelectNotifier = ValueNotifier(0); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: ListView.builder( + itemCount: askDoctorRequestTypeList.length, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(top: 8, bottom: 8), + shrinkWrap: true, + itemBuilder: (context, index) { + return ValueListenableBuilder( + valueListenable: requestTypeSelectNotifier, + builder: (context, duration, child) { + return Theme( + data: Theme.of(context).copyWith( + listTileTheme: ListTileThemeData(horizontalTitleGap: 4), + ), + child: RadioListTile( + title: (askDoctorRequestTypeList[index].description ?? '').toText14(weight: FontWeight.w500), + value: index, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppColors.primaryRedColor; + } + return Color(0xffEEEEEE); + }), + contentPadding: EdgeInsets.only(left: 12.h, right: 12.h), + groupValue: selectedParameterCode, + onChanged: (int? newValue) { + selectedParameterCode = newValue!; + selectedParameterCodeValue = askDoctorRequestTypeList[index].parameterCode!; + requestTypeSelectNotifier.value = selectedParameterCode; + debugPrint(selectedParameterCodeValue.toString()); + }, + ), + ); + }, + ); + }, + ), + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.cancel, + iconColor: AppColors.primaryRedColor, + borderRadius: 12.r, + iconSize: 16.5.h, + fontSize: 16.f, + height: 56.h, + ), + ), + SizedBox(width: 8.h), + Expanded( + child: CustomButton( + text: LocaleKeys.confirm.tr(), + onPressed: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(loadingText: "Sending Request..."); + await myAppointmentsViewModel.sendAskDocCallRequest( + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + requestType: selectedParameterCodeValue.toString(), + remarks: "", + userMobileNumber: myAppointmentsViewModel.appState.getAuthenticatedUser()!.mobileNumber!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Request has been sent successfully, you will be contacted soon.".needTranslation), + callBackFunc: () { + Navigator.of(context).pop(); + }, + title: "", + isCloseButtonVisible: true, + isDismissible: false, + isFullScreen: false, + ); + }, + onError: (errMessage) { + LoaderBottomSheet.hideLoader(); + }); + }, + backgroundColor: AppColors.bgGreenColor.withValues(alpha: 0.20), + borderColor: AppColors.bgGreenColor.withValues(alpha: 0.0), + textColor: AppColors.bgGreenColor, + iconColor: AppColors.bgGreenColor, + icon: AppAssets.confirm, + iconSize: 16.5.h, + borderRadius: 12.r, + fontSize: 16.f, + height: 56.h, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart index c7ef3a9..2f5589c 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart @@ -4,10 +4,8 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; class FacilitySelectionItem extends StatelessWidget { diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart index cacc9d3..cb27f9f 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart @@ -8,8 +8,6 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart' - show MyAppointmentsViewModel; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart index 29a7b96..eed45df 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart @@ -1,63 +1,151 @@ -import 'package:easy_localization/easy_localization.dart' - show tr, StringTranslateExtension; +import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/debouncer.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/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; +import '../../../../features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; +import '../../../emergency_services/call_ambulance/widgets/type_selection_widget.dart' show TypeSelectionWidget; + +// class HospitalBottomSheetBody extends StatelessWidget { +// late BookAppointmentsViewModel appointmentsViewModel; +// late AppointmentViaRegionViewmodel regionalViewModel; +// final TextEditingController searchText = TextEditingController(); +// +// HospitalBottomSheetBody({super.key}); +// +// @override +// Widget build(BuildContext context) { +// appointmentsViewModel = Provider.of(context); +// regionalViewModel = Provider.of(context); +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// LocaleKeys.selectHospital.tr(), +// style: TextStyle( +// fontSize: 21, +// fontWeight: FontWeight.w600, +// color: AppColors.blackColor, +// ), +// ), +// Text( +// LocaleKeys.selectHospitalSubTitle.tr(), +// style: TextStyle( +// fontSize: 16, +// fontWeight: FontWeight.w500, +// color: AppColors.greyTextColor, +// ), +// ), +// SizedBox(height: 16.h), +// TextInputWidget( +// labelText: LocaleKeys.search.tr(), +// hintText: LocaleKeys.searchHospital.tr(), +// controller: searchText, +// onChange: (value) { +// appointmentsViewModel.filterHospitalListByString( +// value, regionalViewModel.selectedRegionId, regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name); +// }, +// isEnable: true, +// prefix: null, +// autoFocus: false, +// isBorderAllowed: false, +// keyboardType: TextInputType.text, +// isAllowLeadingIcon: true, +// selectionType: SelectionTypeEnum.search, +// padding: EdgeInsets.symmetric( +// vertical: ResponsiveExtension(10).h, +// horizontal: ResponsiveExtension(15).h, +// ), +// ), +// SizedBox(height: 24.h), +// // TypeSelectionWidget( +// // hmcCount: "0", +// // hmgCount: "0", +// // ), +// // SizedBox(height: 21.h), +// SizedBox( +// height: MediaQuery.sizeOf(context).height * .4, +// child: ListView.separated( +// itemBuilder: (_, index) { +// var hospital = regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name +// ? appointmentsViewModel.filteredHospitalList!.registeredDoctorMap![regionalViewModel.selectedRegionId!]!.hmgDoctorList![index] +// : appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId!]?.hmcDoctorList?[index]; +// return HospitalListItem( +// hospitalData: hospital, +// isLocationEnabled: appointmentsViewModel.isLocationEnabled(), +// ).onPress(() { +// regionalViewModel.setHospitalModel(hospital); +// if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); +// regionalViewModel.handleLastStepForRegion(); +// } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); +// regionalViewModel.handleLastStepForClinic(); +// } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); +// regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); +// // regionalViewModel.handleLastStepForClinic(); +// } +// }); +// }, +// separatorBuilder: (_, __) => SizedBox( +// height: 16.h, +// ), +// itemCount: (regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name +// ? (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmgDoctorList) +// : (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmcDoctorList)) +// ?.length ?? +// 0), +// ) +// ], +// ); +// } +// } + class HospitalBottomSheetBody extends StatelessWidget { - late BookAppointmentsViewModel appointmentsViewModel; - late AppointmentViaRegionViewmodel regionalViewModel; - final TextEditingController searchText = TextEditingController(); - HospitalBottomSheetBody({super.key}); + final TextEditingController searchText ; + final Debouncer debouncer = Debouncer(milliseconds: 500); + + final int hmcCount; + final int hmgCount; + final List? displayList; + final FacilitySelection selectedFacility; + final Function(FacilitySelection) onFacilityClicked; + final Function(PatientDoctorAppointmentList) onHospitalClicked; + final Function(String) onHospitalSearch; + + HospitalBottomSheetBody({super.key, required this.hmcCount, required this.hmgCount, this.displayList, required this.selectedFacility, required this.onFacilityClicked, required this.onHospitalClicked, required this.onHospitalSearch, required this.searchText}); @override Widget build(BuildContext context) { - appointmentsViewModel = Provider.of(context); - regionalViewModel = Provider.of(context); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - LocaleKeys.selectHospital.tr(), - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.w600, - color: AppColors.blackColor, - ), - ), - Text( - LocaleKeys.selectHospitalSubTitle.tr(), - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColors.greyTextColor, - ), - ), - SizedBox(height: 16.h), TextInputWidget( labelText: LocaleKeys.search.tr(), hintText: LocaleKeys.searchHospital.tr(), controller: searchText, onChange: (value) { - appointmentsViewModel.filterHospitalListByString(value, regionalViewModel.selectedRegionId , regionalViewModel.selectedFacilityType == - FacilitySelection.HMG.name); + debouncer.run((){ + onHospitalSearch(value??""); + }); }, isEnable: true, prefix: null, + autoFocus: false, isBorderAllowed: false, keyboardType: TextInputType.text, @@ -69,59 +157,34 @@ class HospitalBottomSheetBody extends StatelessWidget { ), ), SizedBox(height: 24.h), - // TypeSelectionWidget( - // hmcCount: "0", - // hmgCount: "0", - // ), - // SizedBox(height: 21.h), + TypeSelectionWidget( + selectedFacility:selectedFacility , + hmcCount: hmcCount.toString(), + hmgCount: hmgCount.toString(), + onitemClicked: (selectedValue){ + onFacilityClicked(selectedValue); + }, + ), + SizedBox(height: 21.h), SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( itemBuilder: (_, index) { - var hospital = regionalViewModel.selectedFacilityType == - FacilitySelection.HMG.name - ? appointmentsViewModel - .filteredHospitalList! - .registeredDoctorMap![ - regionalViewModel.selectedRegionId!]! - .hmgDoctorList![index] - : appointmentsViewModel - .filteredHospitalList - ?.registeredDoctorMap?[ - regionalViewModel.selectedRegionId!] - ?.hmcDoctorList?[index]; + var hospital = displayList?[index]; return HospitalListItem( - hospitalData: hospital, - isLocationEnabled: appointmentsViewModel.isLocationEnabled(), - ).onPress(() { - regionalViewModel.setHospitalModel(hospital); - if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); - regionalViewModel.handleLastStepForRegion(); - } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); - regionalViewModel.handleLastStepForClinic(); - } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); - regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID??-1); - // regionalViewModel.handleLastStepForClinic(); - } + hospitalData: hospital, + isLocationEnabled: true, + ).onPress(() { + onHospitalClicked(hospital!); });}, separatorBuilder: (_, __) => SizedBox( - height: 16.h, - ), - itemCount: (regionalViewModel.selectedFacilityType == - FacilitySelection.HMG.name - ? (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[ - regionalViewModel.selectedRegionId]?.hmgDoctorList) - : (appointmentsViewModel - .filteredHospitalList - ?.registeredDoctorMap?[ - regionalViewModel.selectedRegionId]?.hmcDoctorList))?.length ?? - 0), - ) + height: 16.h, + ), + itemCount: displayList?.length ?? 0, + )) ], ); } } + diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart index 58a0d00..a2d22cd 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart @@ -1,21 +1,14 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/enums.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/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class HospitalBottomSheetBodyForDoctorFilter extends StatelessWidget { diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart index 65d779e..b01b541 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart @@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -102,7 +101,7 @@ class HospitalListItem extends StatelessWidget { child: AppCustomChipWidget( labelText: "Location turned off".needTranslation, deleteIcon: AppAssets.location_unavailable, - deleteIconSize: Size(9, 12), + deleteIconSize: Size(9.w, 12.h), textColor: AppColors.blackColor, )), ], diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart index b023f7a..cbf68f6 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart @@ -4,7 +4,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart' show Consumer; diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart index 8b35921..969e23a 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; @@ -61,7 +60,7 @@ class RegionListItem extends StatelessWidget { Widget placesCountItem(String svgPath, String count, String title) { return AppCustomChipWidget( - iconSize: 14, + iconSize: 14.h, icon: svgPath, iconHasColor: false, richText: RichText( diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 6bc8007..6588c2e 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -6,7 +6,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart' show MyAppointmentsViewModel; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem; import 'package:provider/provider.dart'; @@ -80,7 +79,8 @@ class _RegionBottomSheetBodyState extends State { hmgCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmgSize ?? 0}", ).onPress(() { regionalViewModel.setSelectedRegionId(key); - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); + regionalViewModel.setDisplayListAndRegionHospitalList(myAppointmentsVM.hospitalList?.registeredDoctorMap![key]); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); }); }, ), diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 7e5f4a1..c14e957 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -87,10 +87,10 @@ class LoginScreenState extends State { isAllowLeadingIcon: true, padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h), leadingIcon: AppAssets.student_card, - errorMessage: "Please enter a valid national ID or file number", + errorMessage: "Please enter a valid national ID or file number".needTranslation, hasError: false, ), - SizedBox(height: 16.h), // Adjusted to sizer unit (approx 16px) + SizedBox(height: 16.h), CustomButton( height: 50.h, text: LocaleKeys.login.tr(), @@ -109,17 +109,12 @@ class LoginScreenState extends State { } }, ), - SizedBox(height: 10.h), // Adjusted to sizer unit (approx 14px) + SizedBox(height: 10.h), Center( child: RichText( textAlign: TextAlign.center, text: TextSpan( - style: context.dynamicTextStyle( - color: Colors.black, - fontSize: 14.f, // Adjusted to sizer unit - height: 26 / 16, // This height is a ratio, may need re-evaluation - fontWeight: FontWeight.w500, - ), + style: context.dynamicTextStyle(color: Colors.black, fontSize: 14.f, height: 26 / 16, fontWeight: FontWeight.w500), children: [ TextSpan(text: LocaleKeys.dontHaveAccount.tr(), style: context.dynamicTextStyle()), TextSpan(text: " "), @@ -139,9 +134,9 @@ class LoginScreenState extends State { ), ], ), - ).withVerticalPadding(2), // Adjusted to sizer unit + ).withVerticalPadding(2.h), ), - SizedBox(height: 20.h), // Adjusted to sizer unit (approx 14px) + SizedBox(height: 20.h), ], ), ), @@ -154,7 +149,7 @@ class LoginScreenState extends State { required BuildContext context, required TextEditingController? phoneNumberController, required AuthenticationViewModel authViewModel, - }) { + }) async { AppState appState = getIt(); context.showBottomSheet( isScrollControlled: true, diff --git a/lib/presentation/authentication/quick_login.dart b/lib/presentation/authentication/quick_login.dart index f10d84f..bdeb0ff 100644 --- a/lib/presentation/authentication/quick_login.dart +++ b/lib/presentation/authentication/quick_login.dart @@ -1,11 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -21,18 +24,13 @@ class QuickLogin extends StatefulWidget { } class QuickLoginState extends State { + final CacheService cacheService = GetIt.instance(); + @override Widget build(BuildContext context) { NavigationService navigationService = getIt.get(); - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), - ), - ), - padding: const EdgeInsets.all(24), + return Padding( + padding: EdgeInsets.all(24.h), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -104,11 +102,12 @@ class QuickLoginState extends State { borderColor: Color(0xffED1C2B), textColor: Colors.white, icon: AppAssets.apple_finder, + height: 56.h, )), ], ), SizedBox( - height: 16, + height: 16.h, ), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -117,11 +116,13 @@ class QuickLoginState extends State { child: CustomButton( text: LocaleKeys.notNow.tr(), onPressed: () { + cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: false); Navigator.pop(context, "true"); }, backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), textColor: Colors.red, + height: 56.h, // icon: "assets/images/svg/apple-finder.svg", )), ], diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart index 9551ba1..f04f7dd 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -78,8 +78,7 @@ class _RegisterNew extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.showLottie( - context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.h, height: 200.h, fit: BoxFit.cover, repeat: true), + Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.w, height: 200.h, fit: BoxFit.cover, repeat: true), SizedBox(height: 16.h), LocaleKeys.prepareToElevate.tr().toText32(isBold: true), SizedBox(height: 24.h), @@ -113,7 +112,7 @@ class _RegisterNew extends State { Divider(height: 1), TextInputWidget( labelText: LocaleKeys.dob.tr(), - hintText: "11 July, 1994", + hintText: "11 July, 1994".needTranslation, controller: authVm.dobController, focusNode: _dobFocusNode, isEnable: true, @@ -240,7 +239,7 @@ class _RegisterNew extends State { ), ), ), - SizedBox(height: 30), + SizedBox(height: 30.h), ], ), ), diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index 3466497..71cef96 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -69,7 +69,7 @@ class _RegisterNew extends State { height: double.infinity, child: SingleChildScrollView( reverse: false, - padding: EdgeInsets.only(left: 24.h, right: 24.h, top: 0.h), + padding: EdgeInsets.only(left: 24.w, right: 24.w, top: 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -98,7 +98,7 @@ class _RegisterNew extends State { leadingIcon: AppAssets.user_circle, labelColor: AppColors.textColor, ).paddingSymmetrical(0.h, 16.h), - Divider(height: 1, color: AppColors.greyColor), + Divider(height: 1.h, color: AppColors.greyColor), TextInputWidget( labelText: LocaleKeys.nationalIdNumber.tr(), hintText: authVM!.isUserFromUAE() ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : (appState.getNHICUserData.idNumber ?? ""), @@ -130,7 +130,7 @@ class _RegisterNew extends State { hasSelectionCustomIcon: true, isAllowRadius: false, labelColor: AppColors.textColor, - padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 0), + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), selectionCustomIcon: AppAssets.arrow_down, leadingIcon: AppAssets.user_full, ).withVerticalPadding(8); @@ -167,7 +167,7 @@ class _RegisterNew extends State { hasSelectionCustomIcon: true, isAllowRadius: false, labelColor: AppColors.textColor, - padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 0), + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), selectionCustomIcon: AppAssets.arrow_down, leadingIcon: AppAssets.smart_phone, ).withVerticalPadding(8); @@ -188,7 +188,7 @@ class _RegisterNew extends State { leadingIcon: AppAssets.smart_phone, onChange: (value) {}) .paddingSymmetrical(0.h, 16.h), - Divider(height: 1, color: AppColors.greyColor), + Divider(height: 1.h, color: AppColors.greyColor), authVM!.isUserFromUAE() ? Selector? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( selector: (context, authViewModel) { @@ -217,10 +217,10 @@ class _RegisterNew extends State { hasSelectionCustomIcon: true, labelColor: AppColors.textColor, isAllowRadius: false, - padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 0), + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), selectionCustomIcon: AppAssets.arrow_down, leadingIcon: AppAssets.globe, - ).withVerticalPadding(8); + ).withVerticalPadding(8.h); }, ) : TextInputWidget( @@ -256,7 +256,7 @@ class _RegisterNew extends State { leadingIcon: AppAssets.call) .paddingSymmetrical(0.h, 16.h), Divider( - height: 1, + height: 1.h, color: AppColors.greyColor, ), TextInputWidget( @@ -292,9 +292,7 @@ class _RegisterNew extends State { iconColor: AppColors.primaryRedColor, ), ), - SizedBox( - width: 16, - ), + SizedBox(width: 16.w), Expanded( child: CustomButton( backgroundColor: AppColors.primaryRedColor, diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 93b26c7..f0fd8cb 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class SavedLogin extends StatefulWidget { @@ -266,9 +267,15 @@ class _SavedLogin extends State { child: CustomButton( text: LocaleKeys.guest.tr(), onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()), - ); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()) + // ); }, backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart new file mode 100644 index 0000000..5f3fa86 --- /dev/null +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -0,0 +1,185 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_blood_group_widget.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_city_widget.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_gender_widget.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class BloodDonationPage extends StatelessWidget { + BloodDonationPage({super.key}); + + late AppState appState; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, bloodDonationVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.bloodDonation.tr(), + child: Padding( + padding: EdgeInsets.all(24.w), + child: SingleChildScrollView( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.select_city_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.city.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), + (appState.isArabic() + ? (bloodDonationVM.selectedCity.descriptionN ?? LocaleKeys.select.tr()) + : bloodDonationVM.selectedCity.description ?? LocaleKeys.select.tr(context: context)) + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ).onPress(() async { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.selectCity.tr(context: context), + isDismissible: true, + child: SelectCityWidget( + bloodDonationViewModel: bloodDonationVM, + ), + callBackFunc: () {}); + }), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.gender.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), + "Male".toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.selectGender.tr(context: context), + isDismissible: true, + child: SelectGenderWidget( + isArabic: appState.isArabic(), + bloodDonationViewModel: bloodDonationVM, + ), + callBackFunc: () {}); + }), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.bloodType.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), + bloodDonationVM.selectedBloodType.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.select.tr(context: context), + isDismissible: true, + child: SelectBloodGroupWidget( + isArabic: appState.isArabic(), + bloodDonationViewModel: bloodDonationVM, + ), + callBackFunc: () {}); + }), + ], + ), + ), + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomButton( + text: LocaleKeys.save.tr(), + onPressed: () { + // openDoctorScheduleCalendar(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 50.h, + iconSize: 18.h, + ).paddingSymmetrical(16.h, 24.h), + ], + ), + ), + ), + ], + ); + }), + ); + } +} diff --git a/lib/presentation/blood_donation/widgets/city_list_item.dart b/lib/presentation/blood_donation/widgets/city_list_item.dart new file mode 100644 index 0000000..408fb26 --- /dev/null +++ b/lib/presentation/blood_donation/widgets/city_list_item.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/cities_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class CityListItem extends StatelessWidget { + final CitiesModel cityModel; + + late AppState appState; + + CityListItem({super.key, required this.cityModel}); + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [hospitalName], + ), + ), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ); + } + + Widget get hospitalName => Row( + children: [ + Expanded( + child: (appState.isArabic() ? cityModel.descriptionN : cityModel.description)!.toText16(color: AppColors.textColor, isBold: true), + ) + ], + ); +} diff --git a/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart b/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart new file mode 100644 index 0000000..a5ac071 --- /dev/null +++ b/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class SelectBloodGroupWidget extends StatelessWidget { + SelectBloodGroupWidget({super.key, required this.bloodDonationViewModel, required this.isArabic}); + + BloodDonationViewModel bloodDonationViewModel; + bool isArabic; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [bloodDonationViewModel.bloodGroupList[index].name.toText16(color: AppColors.textColor, isBold: true)], + ), + ), + Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h).onPress(() { + bloodDonationViewModel.setSelectedBloodGroup(bloodDonationViewModel.bloodGroupList[index]); + Navigator.of(context).pop(); + })); + }, + separatorBuilder: (_, __) => SizedBox( + height: 8.h, + ), + itemCount: bloodDonationViewModel.bloodGroupList.length), + ) + ], + ); + } +} diff --git a/lib/presentation/blood_donation/widgets/select_city_widget.dart b/lib/presentation/blood_donation/widgets/select_city_widget.dart new file mode 100644 index 0000000..8e2f9a0 --- /dev/null +++ b/lib/presentation/blood_donation/widgets/select_city_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/city_list_item.dart'; + +class SelectCityWidget extends StatelessWidget { + SelectCityWidget({super.key, required this.bloodDonationViewModel}); + + BloodDonationViewModel bloodDonationViewModel; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return CityListItem( + cityModel: bloodDonationViewModel.citiesList[index], + ).onPress(() { + bloodDonationViewModel.setSelectedCity(bloodDonationViewModel.citiesList[index]); + Navigator.of(context).pop(); + }); + }, + separatorBuilder: (_, __) => SizedBox( + height: 8.h, + ), + itemCount: bloodDonationViewModel.citiesList.length), + ) + ], + ); + } +} diff --git a/lib/presentation/blood_donation/widgets/select_gender_widget.dart b/lib/presentation/blood_donation/widgets/select_gender_widget.dart new file mode 100644 index 0000000..67cd4bb --- /dev/null +++ b/lib/presentation/blood_donation/widgets/select_gender_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class SelectGenderWidget extends StatelessWidget { + SelectGenderWidget({super.key, required this.bloodDonationViewModel, required this.isArabic}); + + BloodDonationViewModel bloodDonationViewModel; + bool isArabic; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [bloodDonationViewModel.genderList[index].name.toText16(color: AppColors.textColor, isBold: true)], + ), + ), + Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h).onPress(() { + // bloodDonationViewModel.setSelectedCity(bloodDonationViewModel.citiesList[index]); + Navigator.of(context).pop(); + })); + }, + separatorBuilder: (_, __) => SizedBox( + height: 8.h, + ), + itemCount: bloodDonationViewModel.genderList.length), + ) + ], + ); + } +} diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 02f9f22..76dc1be 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -2,20 +2,25 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart' show RegionBottomSheetBody; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart'; @@ -30,6 +35,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; +// import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; class BookAppointmentPage extends StatefulWidget { const BookAppointmentPage({super.key}); @@ -49,7 +55,7 @@ class _BookAppointmentPageState extends State { void initState() { authVM = context.read(); scheduleMicrotask(() { - bookAppointmentsViewModel.selectedTabIndex = 0; + // bookAppointmentsViewModel.selectedTabIndex = 0; bookAppointmentsViewModel.initBookAppointmentViewModel(); bookAppointmentsViewModel.getLocation(); immediateLiveCareViewModel.initImmediateLiveCare(); @@ -63,34 +69,142 @@ class _BookAppointmentPageState extends State { immediateLiveCareViewModel = Provider.of(context, listen: false); appState = getIt.get(); regionalViewModel = Provider.of(context, listen: true); + getSelectedTabData(bookAppointmentsViewModel.selectedTabIndex); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.bookAppo.tr(context: context), - isLeading: Navigator.of(context).canPop(), - child: SingleChildScrollView( - child: Consumer(builder: (context, bookAppointmentsVM, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, "General".needTranslation), - CustomTabBarModel(null, "LiveCare".needTranslation), - ], - onTabChange: (index) { - bookAppointmentsVM.onTabChanged(index); - }, - ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 24.h), - getSelectedTabData(bookAppointmentsVM.selectedTabIndex), - ], - ); - }), - ), + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.bookAppo.tr(context: context), + isLeading: true, + child: SingleChildScrollView( + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + initialIndex: bookAppointmentsVM.selectedTabIndex, + tabs: [ + CustomTabBarModel(null, "General".needTranslation), + CustomTabBarModel(null, "LiveCare".needTranslation), + ], + onTabChange: (index) { + bookAppointmentsVM.onTabChanged(index); + }, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + getSelectedTabData(bookAppointmentsVM.selectedTabIndex), + SizedBox(height: 24.h), + if (appState.isAuthenticated) ...[ + Consumer(builder: (context, myAppointmentsVM, child) { + return myAppointmentsVM.isPatientMyDoctorsLoading + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true, radius: 50.r), + SizedBox(height: 8.h), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), + ], + ) + : myAppointmentsVM.patientMyDoctorsList.isEmpty + ? SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (appState.isAuthenticated) ...[], + "Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: myAppointmentsVM.patientMyDoctorsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.h, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + SizedBox( + width: 80.w, + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), + ), + ), + ], + ); + }), + ], + ], + ); + }), + ), + ), + ), + _buildSymptomsBottomCard(), + ], ), ); } @@ -212,10 +326,12 @@ class _BookAppointmentPageState extends State { ).paddingSymmetrical(24.h, 0.h); case 1: //TODO: Get LiveCare type Select UI from Hussain - return appState.isAuthenticated - ? Column( - children: [ - Container( + return + // appState.isAuthenticated + // ? + Column( + children: [ + Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.h, @@ -334,13 +450,45 @@ class _BookAppointmentPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h) - : getLiveCareNotLoggedInUI(); + // : getLiveCareNotLoggedInUI() + ; default: SizedBox.shrink(); } return Container(); } + Widget _buildSymptomsBottomCard() { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Not sure? help me choose a clinic!".needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 4.h), + "Mention your symptoms and find the list of doctors accordingly".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ), + ), + SizedBox(width: 16.w), + CustomButton( + height: 40.h, + text: "", + onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection), + icon: AppAssets.arrow_forward, + ) + ], + ).paddingAll(24.w), + ); + } + void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); @@ -364,7 +512,34 @@ class _BookAppointmentPageState extends State { ); } if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { - return HospitalBottomSheetBody(); + return HospitalBottomSheetBody( + searchText: data.searchController, + displayList: data.displayList, + onFacilityClicked: (value) { + data.setSelectedFacility(value); + data.getDisplayList(); + }, + onHospitalClicked: (hospital) { + regionalViewModel.setHospitalModel(hospital); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); + regionalViewModel.handleLastStepForRegion(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinic(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + var appointmentsViewModel = Provider.of(context); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); + } + }, + onHospitalSearch: (value) { + data.searchHospitals(value ?? ""); + }, + selectedFacility: data.selectedFacility, + hmcCount: data.hmcCount, + hmgCount: data.hmgCount, + ); } if (data.bottomSheetState == AppointmentViaRegionState.CLINIC_SELECTION) { // Navigator.of(context).pop(); @@ -476,9 +651,9 @@ class _BookAppointmentPageState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.login1, diff --git a/lib/presentation/book_appointment/doctor_filter/RegionChips.dart b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart index c81e548..19e391e 100644 --- a/lib/presentation/book_appointment/doctor_filter/RegionChips.dart +++ b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart' - show BookAppointmentsViewModel; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart index 155d8e3..982fc54 100644 --- a/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart +++ b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart @@ -2,26 +2,16 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:hmg_patient_app_new/core/enums.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/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_item.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; -import '../../../features/book_appointments/models/resp_models/get_clinic_list_response_model.dart' show GetClinicsListResponseModel; class ClinicBottomSheet extends StatelessWidget { late BookAppointmentsViewModel appointmentsViewModel; diff --git a/lib/presentation/book_appointment/doctor_filter/clinic_item.dart b/lib/presentation/book_appointment/doctor_filter/clinic_item.dart index 0d5ba76..0771d1b 100644 --- a/lib/presentation/book_appointment/doctor_filter/clinic_item.dart +++ b/lib/presentation/book_appointment/doctor_filter/clinic_item.dart @@ -37,7 +37,7 @@ class ClinicItem extends StatelessWidget { Transform.flip( flipX: isArabic, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart index 2785449..e4d11bd 100644 --- a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/RegionChips.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart'; @@ -180,10 +179,10 @@ class DoctorsFilters extends StatelessWidget{ backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: Colors.white, - fontSize: 16, + fontSize: 16.f, padding: EdgeInsets.zero, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 10.r, icon: AppAssets.add_icon, iconColor: AppColors.primaryRedColor, ).paddingAll(24.h), diff --git a/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart index f416ed4..e64c53f 100644 --- a/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart +++ b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.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/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart' - show BookAppointmentsViewModel; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 1223842..5ce7fac 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -1,18 +1,13 @@ -import 'dart:math'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -48,6 +43,7 @@ class DoctorProfilePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network( bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!, @@ -59,9 +55,12 @@ class DoctorProfilePage extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") - .toString() - .toText28(isBold: true), + SizedBox( + width: 220.h, + child: ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") + .toString() + .toText24(isBold: true), + ), (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first : "") .toString() .toText18(weight: FontWeight.w500, color: AppColors.primaryRedColor), @@ -80,18 +79,49 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(height: 12.h), Wrap( direction: Axis.horizontal, - spacing: 3.h, + spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( - icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, - labelText: "Rating: ${bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate}".needTranslation, + labelText: "Branch: ${bookAppointmentsViewModel.doctorsProfileResponseModel.projectName}".needTranslation, ), AppCustomChipWidget( - icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, - labelText: "Reviews: ${bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate}".needTranslation, + labelText: "Clinic: ${bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription}".needTranslation, + ), + ], + ), + SizedBox(height: 24.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.doctor_profile_rating_icon, + width: 48.w, + height: 48.h, + fit: BoxFit.contain, + ), + SizedBox(height: 16.h), + "Ratings".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate.toString().toText16(isBold: true, color: AppColors.textColor), + ], + ), + SizedBox(width: 36.w), + Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.doctor_profile_reviews_icon, + width: 48.w, + height: 48.h, + fit: BoxFit.contain, + ), + SizedBox(height: 16.h), + "Reviews".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate.toString().toText16(isBold: true, color: AppColors.textColor), + ], ), ], ), diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index 440ae45..19a3800 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart' show CapExtension; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; @@ -82,7 +83,7 @@ class LaserAppointment extends StatelessWidget { activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null,LocaleKeys.malE.tr()), - CustomTabBarModel(null,LocaleKeys.female.tr()), + CustomTabBarModel(null, "Female".needTranslation), ], onTabChange: (index) { var viewmodel = context.read(); diff --git a/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart b/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart index d8fa736..baf72d2 100644 --- a/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart +++ b/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.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/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -45,20 +44,22 @@ class BodyTypeListing extends StatelessWidget { ? AppColors.chipPrimaryRedBorderColor : AppColors.blackColor, iconSize: 16, - labelPadding: - EdgeInsetsDirectional.only(start: 8.h, end: 0.h), - padding: - EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.h), - deleteIconSize: Size(18.h, 18.h), - shape: SmoothRectangleBorder( - borderRadius: BorderRadius.circular(10), - smoothness: 10, + labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), + padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.w), + deleteIconSize: Size(18.w, 18.h), + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + smoothness: 10, side: BorderSide( color: index == selectedBodyAreaIndex ? AppColors.chipPrimaryRedBorderColor : AppColors.borderGrayColor, width: 1), - )).onPress(() => onCategoryChanged(index)))) + )).onPress( + () => onCategoryChanged(index), + ), + ), + ) ], ), ); diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index af7d053..2371f4a 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -1,3 +1,4 @@ + import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -13,13 +14,16 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments 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/immediate_livecare_payment_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.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'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -61,7 +65,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { child: Row( children: [ Image.asset( - appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, + appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 52.h, height: 52.h, ), @@ -71,37 +75,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), SizedBox(height: 8.h), - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), - ], - ), - ], - ), - ), - ), - SizedBox(height: 24.h), - "Clinic Information".needTranslation.toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (appState.isArabic() - ? immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceNameN - : immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceName)! - .toText16(isBold: true), - // SizedBox(height: 8.h), - // AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old".needTranslation), + AppCustomChipWidget( + labelText: + "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceName)!}"), + ], + ), ], ), ], @@ -220,15 +204,81 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { isSaudiCurrency: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), ], ).paddingSymmetrical(24.h, 0.h), - CustomButton( - text: LocaleKeys.payNow.tr(context: context), - onPressed: () async { - await askVideoCallPermission().then((val) { - if (val) { - Navigator.of(context).push( - CustomPageRoute( - page: ImmediateLiveCarePaymentPage(), - ), + (immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total == "0.0") + ? CustomButton( + text: LocaleKeys.confirmLiveCare.tr(context: context), + onPressed: () async { + await askVideoCallPermission().then((val) async { + if (val) { + LoaderBottomSheet.showLoader(loadingText: "Confirming LiveCare request, Please wait...".needTranslation); + + await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare("${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}"); + await immediateLiveCareViewModel.getPatientLiveCareHistory(); + LoaderBottomSheet.hideLoader(); + if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } else { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: + "LiveCare requires Camera, Microphone, Location & Notifications permissions to enable virtual consultation between patient & doctor, Please allow these to proceed." + .needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.livecare_book_icon, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h) + : CustomButton( + text: LocaleKeys.payNow.tr(context: context), + onPressed: () async { + await askVideoCallPermission().then((val) { + if (val) { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePaymentPage(), + ), ); } else { showCommonBottomSheetWithoutHeight( @@ -236,16 +286,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { context, child: Utils.getWarningWidget( loadingText: - "LiveCare requires Camera, Microphone & Location permissions to enable virtual consultation between patient & doctor, Please allow these to proceed.".needTranslation, - isShowActionButtons: true, - onCancelTap: () { - Navigator.pop(context); - }, - onConfirmTap: () async { - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, + "LiveCare requires Camera, Microphone, Location & Notifications permissions to enable virtual consultation between patient & doctor, Please allow these to proceed." + .needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, isCloseButtonVisible: true, ); } @@ -272,18 +323,29 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } Future askVideoCallPermission() async { - Map statuses = await [ - Permission.camera, - Permission.microphone, - ].request(); + bool hasPermission = false; + if (Platform.isIOS) { + Map statuses = await [Permission.camera, Permission.microphone].request(); - if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { - // Camera permission granted - return true; + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { + // Camera permission granted + hasPermission = true; + } else { + hasPermission = false; + } } else { - return false; + Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); + + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { + // Camera permission granted + hasPermission = true; + } else { + hasPermission = false; + } } + return hasPermission; + // if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { // return false; // } diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 914ac4a..48e79b1 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -11,30 +11,25 @@ import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -147,7 +142,7 @@ class _ImmediateLiveCarePaymentPageState extends State durationNotifier = ValueNotifier(countdownDuration); - Timer? timer; + // static Duration countdownDuration = Duration(minutes: 1, seconds: 0); + // late ValueNotifier durationNotifier = ValueNotifier(immediateLiveCareViewModel.countdownDuration); + // Timer? timer; @override void initState() { super.initState(); scheduleMicrotask(() { - countdownDuration = Duration(minutes: immediateLiveCareViewModel.patientLiveCareHistoryList[0].watingtimeInteger!, seconds: 0); - durationNotifier = ValueNotifier(countdownDuration); - startTimer(); + // countdownDuration = Duration(minutes: immediateLiveCareViewModel.patientLiveCareHistoryList[0].watingtimeInteger!, seconds: 0); + // durationNotifier = ValueNotifier(immediateLiveCareViewModel.countdownDuration); + // startTimer(); }); } @override void dispose() { - timer?.cancel(); - durationNotifier.dispose(); + // timer?.cancel(); + // durationNotifier.dispose(); super.dispose(); } @@ -85,7 +83,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State( - valueListenable: durationNotifier, + valueListenable: immediateLiveCareVM.durationNotifier, builder: (context, duration, child) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -173,133 +171,133 @@ class _ImmediateLiveCarePendingRequestPageState extends State addTime()); - setState(() {}); - } - - void addTime() { - final seconds = durationNotifier.value.inSeconds - 1; - if (seconds < 0) { - timer?.cancel(); - // Handle end of timer here - // showEndMessage(); - } else { - durationNotifier.value = Duration(seconds: seconds); - } - } - - Future _onWillPop() async { - timer?.cancel(); - Navigator.of(context).pop(); - return true; - } - - Widget buildTime(Duration duration) { - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final hours = twoDigits(duration.inHours); - final minutes = twoDigits(duration.inMinutes.remainder(60)); - final seconds = twoDigits(duration.inSeconds.remainder(60)); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildTimeColumn(hours, "Hours".needTranslation), - buildTimeColumn(minutes, "Mins".needTranslation), - buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), - ], - ); - } - - Widget buildTimeColumn(String time, String label, {bool isLast = false}) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - buildDigit(time[0]), - buildDigit(time[1]), - if (!isLast) buildTimeSeparator(), - ], - ), - buildLabel(label), - ], - ); - } - - Widget buildDigit(String digit) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - // margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: ClipRect( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 600), - switchInCurve: Curves.easeOutExpo, - switchOutCurve: Curves.easeInExpo, - transitionBuilder: (Widget child, Animation animation) { - return Stack( - children: [ - SlideTransition( - position: Tween( - begin: const Offset(0, -1), - end: const Offset(0, 1), - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - )), - child: FadeTransition( - opacity: animation, - child: child, - ), - ), - SlideTransition( - position: Tween( - begin: const Offset(0, -1), - end: const Offset(0, 0), - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.bounceIn, - )), - child: FadeTransition( - opacity: animation, - child: child, - ), - ), - ], - ); - }, - child: Text( - digit, - key: ValueKey(digit), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black, - fontSize: 20.f, - ), - ), - ), - ), - ); - } - - Widget buildLabel(String label) { - return label.toText14(isBold: true); - } - - Widget buildTimeSeparator() { - return const Padding( - padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Text( - ":", - style: TextStyle( - color: Colors.black, - fontSize: 20, - ), - ), - ); - } + // void startTimer() { + // // timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); + // // setState(() {}); + // } + // + // void addTime() { + // final seconds = durationNotifier.value.inSeconds - 1; + // if (seconds < 0) { + // timer?.cancel(); + // // Handle end of timer here + // // showEndMessage(); + // } else { + // durationNotifier.value = Duration(seconds: seconds); + // } + // } + // + // Future _onWillPop() async { + // timer?.cancel(); + // Navigator.of(context).pop(); + // return true; + // } + // + // Widget buildTime(Duration duration) { + // String twoDigits(int n) => n.toString().padLeft(2, '0'); + // final hours = twoDigits(duration.inHours); + // final minutes = twoDigits(duration.inMinutes.remainder(60)); + // final seconds = twoDigits(duration.inSeconds.remainder(60)); + // + // return Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // buildTimeColumn(hours, "Hours".needTranslation), + // buildTimeColumn(minutes, "Mins".needTranslation), + // buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), + // ], + // ); + // } + // + // Widget buildTimeColumn(String time, String label, {bool isLast = false}) { + // return Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Row( + // children: [ + // buildDigit(time[0]), + // buildDigit(time[1]), + // if (!isLast) buildTimeSeparator(), + // ], + // ), + // buildLabel(label), + // ], + // ); + // } + // + // Widget buildDigit(String digit) { + // return Container( + // padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + // // margin: const EdgeInsets.symmetric(horizontal: 2), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(8), + // ), + // child: ClipRect( + // child: AnimatedSwitcher( + // duration: const Duration(milliseconds: 600), + // switchInCurve: Curves.easeOutExpo, + // switchOutCurve: Curves.easeInExpo, + // transitionBuilder: (Widget child, Animation animation) { + // return Stack( + // children: [ + // SlideTransition( + // position: Tween( + // begin: const Offset(0, -1), + // end: const Offset(0, 1), + // ).animate(CurvedAnimation( + // parent: animation, + // curve: Curves.easeOutCubic, + // )), + // child: FadeTransition( + // opacity: animation, + // child: child, + // ), + // ), + // SlideTransition( + // position: Tween( + // begin: const Offset(0, -1), + // end: const Offset(0, 0), + // ).animate(CurvedAnimation( + // parent: animation, + // curve: Curves.bounceIn, + // )), + // child: FadeTransition( + // opacity: animation, + // child: child, + // ), + // ), + // ], + // ); + // }, + // child: Text( + // digit, + // key: ValueKey(digit), + // style: TextStyle( + // fontWeight: FontWeight.bold, + // color: Colors.black, + // fontSize: 20.f, + // ), + // ), + // ), + // ), + // ); + // } + // + // Widget buildLabel(String label) { + // return label.toText14(isBold: true); + // } + // + // Widget buildTimeSeparator() { + // return const Padding( + // padding: EdgeInsets.symmetric(horizontal: 2.0), + // child: Text( + // ":", + // style: TextStyle( + // color: Colors.black, + // fontSize: 20, + // ), + // ), + // ); + // } } diff --git a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart index 8181d25..15598f3 100644 --- a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart @@ -66,35 +66,6 @@ class _SelectImmediateLiveCareClinicPageState extends State(builder: (context, immediateLiveCareVM, child) { return Column( children: [ - // SizedBox(height: 16.h), - // TextInputWidget( - // labelText: LocaleKeys.search.tr(context: context), - // hintText: LocaleKeys.clinicName.tr(context: context), - // controller: searchEditingController, - // isEnable: true, - // prefix: null, - // autoFocus: false, - // isBorderAllowed: false, - // keyboardType: TextInputType.text, - // focusNode: textFocusNode, - // suffix: searchEditingController.text.isNotEmpty - // ? GestureDetector( - // onTap: () { - // searchEditingController.clear(); - // bookAppointmentsViewModel.filterClinics(""); - // textFocusNode.unfocus(); - // }, - // child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - // ) - // : null, - // onChange: (value) { - // bookAppointmentsViewModel.filterClinics(value!); - // }, - // padding: EdgeInsets.symmetric( - // vertical: ResponsiveExtension(10).h, - // horizontal: ResponsiveExtension(15).h, - // ), - // ), ListView.separated( padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, diff --git a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart index 7385c6b..cdf13c6 100644 --- a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart +++ b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart @@ -7,11 +7,11 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; 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/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class LiveCareClinicCard extends StatelessWidget { LiveCareClinicCard({super.key, required this.liveCareClinicListResponseModel, required this.isLoading, required this.immediateLiveCareViewModel}); @@ -34,20 +34,12 @@ class LiveCareClinicCard extends StatelessWidget { children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 24.h, height: 24.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading), - Column( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.livecare_online_icon, - width: 16.h, - height: 16.h, - fit: BoxFit.contain, - iconColor: liveCareClinicListResponseModel.isOnline == 1 ? AppColors.successColor : AppColors.primaryRedColor) - .toShimmer2(isShow: isLoading), - SizedBox(height: 4.h), - liveCareClinicListResponseModel.isOnline == 1 - ? LocaleKeys.online.tr(context: context).toText10(isBold: true, color: AppColors.successColor).toShimmer2(isShow: isLoading) - : "Offline".toText10(isBold: true, color: AppColors.primaryRedColor).toShimmer2(isShow: isLoading), - ], + AppCustomChipWidget( + icon: AppAssets.livecare_online_icon, + iconColor: AppColors.whiteColor, + richText: (liveCareClinicListResponseModel.isOnline == 1 ? LocaleKeys.online.tr(context: context) : "Offline".needTranslation).toText10(isBold: true, color: AppColors.whiteColor), + backgroundColor: liveCareClinicListResponseModel.isOnline == 1 ? AppColors.successColor : AppColors.primaryRedColor, + textColor: AppColors.whiteColor, ), ]), SizedBox(height: 8.h), diff --git a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart index 226ac50..31fb71f 100644 --- a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart +++ b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart @@ -1,11 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class SelectLiveCareCallType extends StatelessWidget { @@ -16,46 +17,30 @@ class SelectLiveCareCallType extends StatelessWidget { @override Widget build(BuildContext context) { //TODO: Replace with actual icons - return GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - mainAxisExtent: 130, - ), - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - shrinkWrap: true, + return Column( children: [ - MedicalFileCard( - label: "Video Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, - isLargeText: true, - iconSize: 36.h, + checkInOptionCard( + AppAssets.checkin_location_icon, + "Video Call".needTranslation, + "View your nearest HMG locations".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(1); }), - MedicalFileCard( - label: "Audio Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.allergy_info_icon, - isLargeText: true, - iconSize: 36.h, + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + "Audio Call".needTranslation, + "Provide your feedback on our services".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(2); }), - MedicalFileCard( - label: "Phone Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.vaccine_info_icon, - isLargeText: true, - iconSize: 36.h, + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + "Phone Call".needTranslation, + "Live chat option with HMG".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(3); @@ -63,4 +48,45 @@ class SelectLiveCareCallType extends StatelessWidget { ], ); } + + Widget checkInOptionCard(String icon, String title, String subTitle) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(isBold: true, color: AppColors.textColor), + subTitle.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ), + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ), + ], + ).paddingAll(16.h), + ); + } } diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 0cb4b2d..72658bf 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -12,11 +13,14 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -46,148 +50,180 @@ class _ReviewAppointmentPageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.reviewAppointment.tr(context: context), - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - LocaleKeys.docInfo.tr(context: context).toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Image.network( - bookAppointmentsViewModel.selectedDoctor.doctorImageURL!, - width: 50.h, - height: 50.h, - fit: BoxFit.cover, - ).circle(100), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.49, - child: - "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}".toString().toText16(isBold: true, maxlines: 1), - ), - Image.network( - bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!, - width: 20.h, - height: 15.h, - fit: BoxFit.cover, - ), - ], - ), - SizedBox(height: 2.h), - (bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "") - .toString() - .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1), - ], - ), - ], - ), - SizedBox(height: 12.h), - Wrap( - direction: Axis.horizontal, - spacing: 8.h, - runSpacing: 8.h, - children: [ - AppCustomChipWidget( - labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation, - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation, - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.selectedAppointmentDate}".needTranslation, - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.selectedAppointmentTime}".needTranslation, - ), - ], - ), - ], + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + LocaleKeys.docInfo.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, ), - ), - ), - SizedBox(height: 24.h), - LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Image.asset( - appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, - width: 52.h, - height: 52.h, - ), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), - SizedBox(height: 8.h), - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), - ], - ), - ], + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.network( + bookAppointmentsViewModel.selectedDoctor.doctorImageURL!, + width: 50.h, + height: 50.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.49, + child: "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}" + .toString() + .toText16(isBold: true, maxlines: 1), + ), + Image.network( + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!, + width: 20.h, + height: 15.h, + fit: BoxFit.cover, + ), + ], + ), + SizedBox(height: 2.h), + (bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "") + .toString() + .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1), + ], + ), + ], + ), + SizedBox(height: 12.h), + Wrap( + direction: Axis.horizontal, + spacing: 8.h, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation, + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation, + ), + AppCustomChipWidget( + labelText: + "${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? DateUtil.formatDateToDate(DateTime.now(), false) : bookAppointmentsViewModel.selectedAppointmentDate}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Waiting Appointment".needTranslation : bookAppointmentsViewModel.selectedAppointmentTime}" + .needTranslation, + ), + ], + ), + ], + ), ), ), - ), - SizedBox(height: 24.h), - "Hospital Information".needTranslation.toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, + SizedBox(height: 24.h), + LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Image.asset( + appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, + width: 52.h, + height: 52.h, + ), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), + SizedBox(height: 8.h), + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + ], + ), + ], + ), + ), ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true), - ], + SizedBox(height: 24.h), + "Hospital Information".needTranslation.toText16(isBold: true), + SizedBox(height: 16.h), + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true), + SizedBox(height: 8.h), + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null + ? Wrap( + direction: Axis.horizontal, + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" + : "Floor: ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", + ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + AppCustomChipWidget( + labelText: + "Nearest Gate: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") + .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + ], + ) + : SizedBox.shrink(), + ], + ), ), ), - ), - ], - ), - ), + ], + ), + ); + }), ), ), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 24.h, + borderRadius: 24.r, hasShadow: true, ), child: CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), + text: bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Add to waiting list" : LocaleKeys.bookAppo.tr(context: context), onPressed: () async { - initiateBookAppointment(); + if (bookAppointmentsViewModel.isWaitingAppointmentSelected) { + getWalkInAppointmentPatientShare(); + } else { + initiateBookAppointment(); + } }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, @@ -204,6 +240,27 @@ class _ReviewAppointmentPageState extends State { ); } + void getWalkInAppointmentPatientShare() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Appointment Share...".needTranslation); + await bookAppointmentsViewModel.getWalkInPatientShareAppointment(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: WaitingAppointmentPaymentPage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } + void initiateBookAppointment() async { LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation); myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 5949ef5..d008497 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -2,8 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -21,7 +19,6 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import '../../features/book_appointments/models/resp_models/doctors_list_response_model.dart'; @@ -46,7 +43,7 @@ class _SearchDoctorByNameState extends State { body: Column( children: [ Expanded( - child: CollapsingListView( + child: CollapsingListView( title: "Choose Doctor".needTranslation, child: SingleChildScrollView( child: Padding( @@ -250,9 +247,9 @@ class _SearchDoctorByNameState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.search_icon, diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index d0bd740..48c0cd5 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -27,17 +28,19 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class SelectClinicPage extends StatefulWidget { bool isFromRegionFlow; + HealthCalculatorsTypeEnum? calculatorType; + dynamic calculatedResult; - SelectClinicPage({super.key, this.isFromRegionFlow = false}); + SelectClinicPage({super.key, this.isFromRegionFlow = false, this.calculatorType, this.calculatedResult}); @override State createState() => _SelectClinicPageState(); @@ -69,131 +72,873 @@ class _SelectClinicPageState extends State { bookAppointmentsViewModel = Provider.of(context, listen: false); regionalViewModel = Provider.of(context, listen: true); appState = getIt.get(); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: bookAppointmentsViewModel.isLiveCareSchedule ? "Select LiveCare Clinic".needTranslation : LocaleKeys.selectClinic.tr(context: context), - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Consumer(builder: (context, bookAppointmentsVM, child) { - return Column( - children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.clinicName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, + if (widget.calculatorType != null) { + return CollapsingListView( + title: "Your ${widget.calculatorType!.name.toCamelCase}", + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + child: CustomButton( + text: LocaleKeys.bookAppo.tr(), + onPressed: () { + if (widget.calculatedResult["clinicId"] == null) { + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + bookAppointmentsViewModel.isGetDocForHealthCal = true; + bookAppointmentsViewModel.calculationID = widget.calculatedResult["calculationID"]; + Navigator.push(context, CustomPageRoute(page: SelectDoctorPage())); + } else { + onClinicSelected(GetClinicsListResponseModel(clinicID: widget.calculatedResult["clinicId"], clinicDescription: "asdfds", clinicDescriptionN: "asdfds")); + } + }, + icon: null, + fontSize: 16.f, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + borderRadius: 12.r, + fontWeight: FontWeight.w500), + ), + child: getCalculatorResultWidget(type: widget.calculatorType!, calculatedResult: widget.calculatedResult).paddingSymmetrical(18.w, 24.h), + ); + } else { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: bookAppointmentsViewModel.isLiveCareSchedule ? "Select LiveCare Clinic".needTranslation : LocaleKeys.selectClinic.tr(context: context), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + children: [ + SizedBox(height: 16.h), + TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.clinicName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), ), - ), - bookAppointmentsVM.isLiveCareSchedule - ? ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); - }), + bookAppointmentsVM.isLiveCareSchedule + ? ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); + }), + ), ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ) - : ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); - }), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ) + : ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); + }), + ), ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + ], + ); + }), + ), + ), + ), + ); + } + } + + Widget getCalculatorResultWidget({required HealthCalculatorsTypeEnum type, dynamic calculatedResult}) { + switch (widget.calculatorType!) { + case HealthCalculatorsTypeEnum.bmi: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.calories: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bmr: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.idealBodyWeight: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bodyFat: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.crabsProteinFat: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.ovulation: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.deliveryDueDate: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bloodSugar: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bloodCholesterol: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.triglycerides: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + } + } + + Widget resultWidget({required HealthCalculatorsTypeEnum type, dynamic calculatedResult}) { + return Column( + children: [ + if (type == HealthCalculatorsTypeEnum.bmi) ...[ + if (calculatedResult["bmiCategory"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'Your BMI is within the healthy range. Continue a balanced diet, regular physical activity, and routine checkups to maintain good health. Monitor any significant changes over time.', + color: Colors.green, + icon: Icons.scale, + hide: true, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmiCategory"] == "Underweight") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'BMI suggests you are underweight. Consider increasing calorie intake with nutrient-dense foods, include resistance training to build muscle, and consult a healthcare professional to check for underlying causes.', + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmiCategory"] == "Overweight") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'BMI indicates overweight. Aim for gradual, sustainable weight loss through a modest calorie deficit, regular physical activity, and healthier food choices. Seek medical advice before major lifestyle changes.', + color: Colors.red, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.calories) ...[ + if (calculatedResult["calorieRange"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'Estimated daily calories to maintain your current weight given your activity level and metabolism. Use this as a baseline: increase slightly for muscle gain, or create a small deficit for gradual weight loss. Track progress and adjust every 2–4 weeks.', + color: Colors.green, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["calorieRange"] == "Low") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'A moderate calorie reduction designed for steady, sustainable weight loss (typically 0.25–0.5 kg per week). Combine with resistance training and adequate protein to preserve muscle. Avoid extreme cuts and consult a professional if you have medical conditions.', + color: Colors.orange, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["calorieRange"] == "High") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'A large calorie reduction that may produce faster weight loss but can be hard to sustain and increase risk of nutrient deficiencies, fatigue, or muscle loss. Not recommended long-term; seek guidance from a healthcare or nutrition professional before continuing.', + color: Colors.red, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.bmr) ...[ + if (calculatedResult["bmrRange"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your Basal Metabolic Rate (BMR) is within the expected range for your profile. BMR is the energy your body needs at rest to maintain vital functions (breathing, circulation, temperature). Use this value as the baseline for estimating total daily energy needs by applying an activity multiplier. Maintain a balanced diet and regular physical activity to support metabolic health.', + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmrRange"] == "Low") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your BMR is lower than typical for your profile. Possible causes include lower lean muscle mass, age-related metabolic decline, or metabolic adaptation from prolonged calorie restriction. Consider focusing on resistance training to build/maintain muscle, ensure adequate protein and micronutrient intake, and consult a healthcare professional if this is unexpected.', + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmrRange"] == "High") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your BMR is higher than average for your profile. This can reflect greater lean mass, younger age, or a naturally higher metabolic rate, meaning you burn more energy at rest. Use this information to tailor calorie targets: higher needs for maintenance or muscle gain, moderate deficit for weight loss. Seek medical advice if you notice rapid unexplained changes.', + color: Colors.red, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.idealBodyWeight) ...[ + if (calculatedResult["status"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + if (diff == 0) { + return 'Your weight is at the ideal target. Maintain your current habits — balanced diet and regular activity — to keep this.'; + } else if (diff > 0) { + return 'You are below the ideal weight by ${diff.toStringAsFixed(1)} kg. Aim to gradually increase weight with a modest calorie surplus, nutrient-dense foods, and resistance training.'; + } else { + return 'You are above the ideal weight by ${diff.abs().toStringAsFixed(1)} kg. Aim for a gradual, sustainable weight reduction through a moderate calorie deficit, increased activity, and balanced nutrition.'; + } + } + return 'Your weight is close to the ideal range. Maintain a balanced diet and regular activity; follow personalized advice from a healthcare professional if needed.'; + })(), + color: Colors.green, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["status"] == "Underweight") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + return 'You are below the ideal weight by ${diff.toStringAsFixed(1)} kg. Focus on increasing calorie intake with nutrient-dense foods, prioritize protein and resistance training to build healthy mass, and consider consulting a healthcare professional.'; + } + return 'You appear underweight compared to the ideal. Consider increasing calorie intake with nutrient-dense foods and resistance training; seek professional guidance if needed.'; + })(), + color: Colors.orange, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["status"] == "Overweight") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + return 'You are above the ideal weight by ${diff.abs().toStringAsFixed(1)} kg. Aim for a gradual, sustainable weight loss strategy — moderate calorie deficit, regular physical activity, and balanced nutrition. Consult a professional before major changes.'; + } + return 'You appear above the ideal weight. Consider a gradual, sustainable calorie deficit combined with activity and balanced nutrition; consult a professional if needed.'; + })(), + color: Colors.red, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.crabsProteinFat) ...[ + if (calculatedResult["dietType"] == "Very Low Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Very low‑carb (ketogenic) approach: minimal carbohydrates, higher fat and moderate protein. May support rapid weight loss and improved blood sugar control for some, but can be hard to sustain and may cause nutrient gaps. Monitor hydration/electrolytes and consult a professional for long‑term use.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown: Carbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Low Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Low‑carb, higher‑protein plan: reduced carbohydrates with increased protein to support satiety and muscle maintenance. Helpful for weight management and glycemic control when balanced with vegetables and healthy fats. Ensure adequate fiber and micronutrients.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Moderate Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Moderate carbohydrate intake with balanced protein and fats: provides steady energy for daily activity and exercise while allowing flexibility. Suitable for many people aiming for sustainable weight management and performance.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "USDA Guidelines ") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'USDA‑based balanced pattern: emphasizes fruits, vegetables, whole grains, lean proteins, and limited added sugars and saturated fats. Evidence‑based framework for general health, nutrient adequacy, and chronic disease prevention.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Zone Diet") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Zone Diet (~40% carbs / 30% protein / 30% fat): emphasizes hormonal balance and portion control. May improve body composition and energy for some, but requires planning; personalize with a nutrition professional.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.ovulation) ...[ + Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.nextPeriodAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["lastPeriodDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], ), - ], - ); - }), + ], + ), + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["lastPeriodDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + SizedBox(height: 16.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["ovulationDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["ovulationDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + SizedBox(height: 16.h), + "Fertility Window".toText16(color: AppColors.textColor, weight: FontWeight.w600), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["fertileWindowStart"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["fertileWindowEnd"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + "This is your fertile window. Maintaining a healthy calorie range supports your body during this period, considering your Basal Metabolic Rate and activity. Your Body Mass Index is within the healthy range." + .toText12(height: 1.5, color: AppColors.textColorLight, fontWeight: FontWeight.w500) + ], + ), + ), + SizedBox( + height: 16.h, + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bulb), + SizedBox( + width: 8.w, + ), + Row( + children: [ + "Tips to keep in mind".toText16(color: AppColors.textColor, weight: FontWeight.w600), + ], + ), + ], + ), + SizedBox(height: 8.h), + Column( + children: [ + _buildTipRow("Drink before you feel thirsty."), + SizedBox(height: 4.h), + _buildTipRow("Keep a refillable bottle next to you."), + SizedBox(height: 4.h), + _buildTipRow("Track your daily intake to stay motivated."), + SizedBox(height: 4.h), + _buildTipRow("Choose sparkling water instead of soda."), + ], + ).paddingOnly(left: 4.w, right: 4.w) + ], + ), + ), + ], + ) + ] else if (type == HealthCalculatorsTypeEnum.deliveryDueDate) ...[ + Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDueDateAccordion, width: 30.h, height: 30.h), + SizedBox(width: 8.w), + calculatedResult["dueDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + Expanded(child: SizedBox(width: 8.w)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["dueDateDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "First Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["firstTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["firstTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["firstTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Second Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["secondTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["secondTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["secondTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Third Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["thirdTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDayAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["thirdTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["thirdTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + ], + ) + ], + ], + ); + } + + Widget _buildTipRow(String text) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 6.h, right: 8.w), + child: CircleAvatar( + radius: 3.r, + backgroundColor: AppColors.textColorLight, ), ), + Expanded( + child: text.toText12(color: AppColors.textColorLight, fontWeight: FontWeight.w500, height: 1.2.h), + ), + ], + ); + } + + Widget calorieWidget({required String title, required dynamic calories, required String description, required Color color, required IconData icon, bool hide = false, List? chipsItems}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ('${calories.toStringAsFixed(0)}').toText32(color: getTextColor(title), isBold: true), + SizedBox(width: 10.w), + hide ? SizedBox() : 'kcal/day'.toText12(color: AppColors.inputLabelTextColor, isBold: true, fontWeight: FontWeight.w600) + ], + ), + _buildStatusIcon(title) + ], + ), + SizedBox(height: 16.h), + if (chipsItems != null && chipsItems.isNotEmpty) ...[ + Wrap( + spacing: 8.0, // Horizontal space between chips + runSpacing: 4.0, // Vertical space between rows of chips + children: chipsItems.map((item) { + return CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: item, + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor) + .paddingOnly(bottom: 4.h); + }).toList(), + ), + SizedBox(height: 8.h), + ], + title.toText16(color: AppColors.textColor, weight: FontWeight.w600), + SizedBox(height: 8.h), + description.toText12(height: 1.2.h, color: AppColors.textColorLight, fontWeight: FontWeight.w500) + ], ), ); } + Widget _buildStatusIcon(String title) { + final key = title.trim(); + if (key == 'Maintenance Calories' || key == 'Normal' || key == 'Ideal Weight' || key == 'Moderate Crabs' || key == 'USDA Guidelines') { + return Utils.buildSvgWithAssets(icon: AppAssets.checkmark_icon, width: 24.w, height: 24.h); + } else if (key == 'Moderate Deficit' || key == 'Low' || key == 'Underweight' || key == 'Very Low Crabs' || key == 'Low Crabs' || key == 'Zone Diet') { + return Utils.buildSvgWithAssets(icon: AppAssets.trade_down_yellow, width: 24.w, height: 24.h); + } else if (key == 'Aggressive Deficit' || key == 'High' || key == 'Overweight' || key == "Obese") { + return Utils.buildSvgWithAssets(icon: AppAssets.trade_down_red, width: 24.w, height: 24.h); + } else { + return SizedBox(); + } + } + + Color getTextColor(String title) { + final key = title.trim(); + if (key == 'Maintenance Calories' || key == 'Normal' || key == 'Ideal Weight' || key == 'Moderate Crabs' || key == 'USDA Guidelines') { + return AppColors.successColor; + } else if (key == 'Moderate Deficit' || key == 'Low' || key == 'Underweight' || key == 'Very Low Crabs' || key == 'Low Crabs' || key == 'Zone Diet') { + return AppColors.warningColor; + } else if (key == 'Aggressive Deficit' || key == 'High' || key == 'Overweight' || key == "Obese") { + return AppColors.errorColor; + } else { + return Colors.black; + } + } + void onLiveCareClinicSelected(GetLiveCareClinicsResponseModel clinic) { bookAppointmentsViewModel.setLiveCareSelectedClinic(clinic); bookAppointmentsViewModel.setIsDoctorsListLoading(true); @@ -210,9 +955,12 @@ class _SelectClinicPageState extends State { if (clinic.isLiveCareClinicAndOnline ?? false) { Navigator.of(context).push( CustomPageRoute( - page: SelectLivecareClinicPage(onNegativeClicked: (){ + page: SelectLivecareClinicPage( + onNegativeClicked: () { handleDoctorScreen(clinic); - },), + }, + selectedClinic: clinic, + ), ), ); } else { @@ -226,9 +974,7 @@ class _SelectClinicPageState extends State { //Dental Clinic Flow if (clinic.clinicID == 17) { if (appState.isAuthenticated) { - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel - .currentlySelectedHospitalFromRegionFlow ?? - "0")); + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); return; } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); @@ -239,8 +985,7 @@ class _SelectClinicPageState extends State { ); return; } - } - else if (clinic.clinicID == 253) { + } else if (clinic.clinicID == 253) { bookAppointmentsViewModel.resetLaserData(); bookAppointmentsViewModel.getLaserClinic(); Navigator.push( @@ -267,21 +1012,19 @@ class _SelectClinicPageState extends State { } } - void openRegionListBottomSheet( - BuildContext context, RegionBottomSheetType type) { + void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { bookAppointmentsViewModel.setProjectID(null); regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, - child: Consumer(builder: (_, data, __) { - return getRegionalSelectionWidget(data); - }), callBackFunc: () { - }); + child: Consumer(builder: (context, data, __) { + return getRegionalSelectionWidget(data, context); + }), callBackFunc: () {}); } - Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) { + Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) { if (data.bottomSheetState == AppointmentViaRegionState.REGION_SELECTION) { return RegionBottomSheetBody(); } @@ -292,7 +1035,34 @@ class _SelectClinicPageState extends State { ); } if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { - return HospitalBottomSheetBody(); + return HospitalBottomSheetBody( + searchText: data.searchController, + displayList: data.displayList, + onFacilityClicked: (value) { + data.setSelectedFacility(value); + data.getDisplayList(); + }, + onHospitalClicked: (hospital) { + regionalViewModel.setHospitalModel(hospital); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); + regionalViewModel.handleLastStepForRegion(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinic(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + var appointmentsViewModel = Provider.of(context, listen: false); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); + } + }, + onHospitalSearch: (value) { + data.searchHospitals(value ?? ""); + }, + selectedFacility: data.selectedFacility, + hmcCount: data.hmcCount, + hmgCount: data.hmgCount, + ); } if (data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION) { //if the region screen is opened for the dental clinic then the project id will be in the hospital list as the list is formed form the get project api @@ -306,7 +1076,7 @@ class _SelectClinicPageState extends State { if (appState.isAuthenticated) { initDentalAppointment(); return SizedBox.shrink(); - }else { + } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); } } @@ -319,8 +1089,7 @@ class _SelectClinicPageState extends State { } bookAppointmentsViewModel.setProjectID(id); return SizedBox.shrink(); - } - else { + } else { return SizedBox.shrink(); } return SizedBox.shrink(); @@ -474,9 +1243,7 @@ class _SelectClinicPageState extends State { void initDentalAppointment() async { await Future.delayed(Duration(milliseconds: 300)); - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel - .currentlySelectedHospitalFromRegionFlow ?? - "0")); + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); return; } } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 46c3774..d77e152 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.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'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; @@ -32,20 +33,30 @@ class SelectDoctorPage extends StatefulWidget { class _SelectDoctorPageState extends State { TextEditingController searchEditingController = TextEditingController(); + int? expandedIndex; FocusNode textFocusNode = FocusNode(); late AppState appState; late BookAppointmentsViewModel bookAppointmentsViewModel; + // Scroll controller to control page scrolling when a group expands + late ScrollController _scrollController; + // Map of keys for each item to allow scrolling to them + final Map _itemKeys = {}; + @override void initState() { + _scrollController = ScrollController(); scheduleMicrotask(() { + bookAppointmentsViewModel.setIsNearestAppointmentSelected(false); if (bookAppointmentsViewModel.isLiveCareSchedule) { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { + bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { bookAppointmentsViewModel.getDoctorsList(); } @@ -54,6 +65,12 @@ class _SelectDoctorPageState extends State { super.initState(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); @@ -63,6 +80,7 @@ class _SelectDoctorPageState extends State { body: CollapsingListView( title: "Choose Doctor".needTranslation, child: SingleChildScrollView( + controller: _scrollController, child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), child: Consumer(builder: (context, bookAppointmentsVM, child) { @@ -105,23 +123,80 @@ class _SelectDoctorPageState extends State { ), ], ), + SizedBox(height: 16.h), + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + bookAppointmentsVM.setIsSortByClinic(true); + }, + backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + textColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + bookAppointmentsVM.setIsSortByClinic(false); + }, + backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.textColor.withOpacity(0.2) : AppColors.primaryRedColor, + textColor: bookAppointmentsVM.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(0.h, 0.h), + SizedBox(height: 16.h), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.nearestAppo.tr(context: context).toText13(isBold: true), + SizedBox(height: 4.h), + "View nearest available appointments".needTranslation.toText11(color: AppColors.textColorLight, weight: FontWeight.w500), + ], + ), + const Spacer(), + Switch( + activeThumbColor: AppColors.successColor, + activeTrackColor: AppColors.successColor.withValues(alpha: .15), + value: bookAppointmentsVM.isNearestAppointmentSelected, + onChanged: (newValue) async { + bookAppointmentsVM.setIsNearestAppointmentSelected(newValue); + + }, + ), + ], + ), ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 - : (bookAppointmentsVM.isLiveCareSchedule - ? (bookAppointmentsVM.liveCareDoctorsList.isNotEmpty ? bookAppointmentsVM.liveCareDoctorsList.length : 1) - : (bookAppointmentsVM.doctorsList.isNotEmpty ? bookAppointmentsVM.doctorsList.length : 1)), + : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), isLoading: true, bookAppointmentsViewModel: bookAppointmentsViewModel, ) - : checkIsDoctorsListEmpty() + : bookAppointmentsVM.doctorsListGrouped.isEmpty ? Utils.getNoDataWidget(context, noDataText: "No Doctor found for selected criteria...".needTranslation) : AnimationConfiguration.staggeredList( position: index, @@ -130,40 +205,107 @@ class _SelectDoctorPageState extends State { verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( + key: _itemKeys.putIfAbsent(index, () => GlobalKey()), duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: - RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.isLiveCareSchedule - ? bookAppointmentsVM.liveCareDoctorsList[index] - : bookAppointmentsVM.doctorsList[index], - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.isLiveCareSchedule - ? bookAppointmentsVM.liveCareDoctorsList[index] - : bookAppointmentsVM.doctorsList[index]); - // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + // After rebuild, ensure the expanded item is visible + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _itemKeys[index]; + if (key != null && key.currentContext != null && expandedIndex == index) { + Scrollable.ensureVisible( + key.currentContext!, + duration: Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + }); + }, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row with count badge and expand/collapse icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${bookAppointmentsVM.doctorsListGrouped[index].length} ${'doctors'.needTranslation}", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + // Clinic/Hospital name as group title + Text( + bookAppointmentsVM.isSortByClinic + ? (bookAppointmentsVM.doctorsListGrouped[index].first.clinicName ?? 'Unknown') + : (bookAppointmentsVM.doctorsListGrouped[index].first.projectName ?? 'Unknown'), + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + // Expanded content - list of doctors in this group + AnimatedSwitcher( + duration: Duration(milliseconds: 400), + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.only(top: 12.h), + child: Column( + children: bookAppointmentsVM.doctorsListGrouped[index].map((doctor) { + return Container( + margin: EdgeInsets.only(bottom: 12.h), + child: DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ); + }).toList(), + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ), diff --git a/lib/presentation/book_appointment/select_livecare_clinic_page.dart b/lib/presentation/book_appointment/select_livecare_clinic_page.dart index 12a99bc..502e38d 100644 --- a/lib/presentation/book_appointment/select_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/select_livecare_clinic_page.dart @@ -1,22 +1,37 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; class SelectLivecareClinicPage extends StatelessWidget { final VoidCallback? onNegativeClicked; - const SelectLivecareClinicPage({super.key, this.onNegativeClicked}); + late GetClinicsListResponseModel selectedClinic; + + SelectLivecareClinicPage({super.key, this.onNegativeClicked, required this.selectedClinic}); + + late ImmediateLiveCareViewModel immediateLiveCareViewModel; @override Widget build(BuildContext context) { + immediateLiveCareViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -33,7 +48,7 @@ class SelectLivecareClinicPage extends StatelessWidget { SizedBox(height: 40.h), Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.h, height: 58.h), + Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.w, height: 58.h), SizedBox(width: 18.h), Expanded( child: Column( @@ -103,13 +118,42 @@ class SelectLivecareClinicPage extends StatelessWidget { children: [ CustomButton( text: "Yes please, I am in a hurry".needTranslation, - onPressed: () {}, + onPressed: () async { + Navigator.pop(context); + GetLiveCareClinicListResponseModel liveCareClinic = GetLiveCareClinicListResponseModel( + iD: selectedClinic.liveCareClinicID, + serviceID: selectedClinic.liveCareServiceID, + serviceName: selectedClinic.clinicDescription, + serviceNameN: selectedClinic.clinicDescriptionN, + ); + + immediateLiveCareViewModel.setLiveCareSelectedCallType(1); + immediateLiveCareViewModel.setImmediateLiveCareSelectedClinic(liveCareClinic); + LoaderBottomSheet.showLoader(loadingText: "Fetching fees, Please wait...".needTranslation); + await immediateLiveCareViewModel.getLiveCareImmediateAppointmentFees(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ImmediateLiveCarePaymentDetails(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.livecare_book_icon, @@ -122,18 +166,13 @@ class SelectLivecareClinicPage extends StatelessWidget { onPressed: () { Navigator.of(context).pop(); onNegativeClicked?.call(); - // Navigator.of(context).push( - // CustomPageRoute( - // page: SelectDoctorPage(), - // ), - // ); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, ).paddingSymmetrical(24.h, 0.h), diff --git a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart new file mode 100644 index 0000000..f832db6 --- /dev/null +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart @@ -0,0 +1,118 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class WaitingAppointmentInfo extends StatelessWidget { + WaitingAppointmentInfo({super.key}); + + late BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Waiting Appointment".needTranslation, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.waiting_appointment_icon, width: 48.h, height: 48.h, fit: BoxFit.contain), + SizedBox(height: 16.h), + "What is Waiting Appointment?".needTranslation.toText16(isBold: true), + SizedBox(height: 16.h), + "The waiting appointments feature allows you to book an appointment while you are inside the hospital building, and in case there is no available slot in the doctor’s schedule." + .needTranslation + .toText14(isBold: false), + SizedBox(height: 16.h), + "The appointment with the doctor is confirmed, but the time of entry is uncertain.".needTranslation.toText14(isBold: false), + SizedBox(height: 24.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.warning, + size: 20, + color: AppColors.warningColorYellow, + ), + SizedBox(width: 10.w), + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: "Note: You must have to pay within 10 minutes of booking, otherwise your appointment will be cancelled automatically" + .needTranslation + .toText14(isBold: true, color: AppColors.warningColorYellow), + ), + ], + ), + ], + ), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: CustomButton( + text: "Continue".needTranslation, + onPressed: () async { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.onlineCheckIn.tr(), + child: WaitingAppointmentOnlineCheckinSheet( + bookAppointmentsViewModel: bookAppointmentsViewModel, + ), + callBackFunc: () {}, + isFullScreen: false); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.calendar, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart new file mode 100644 index 0000000..4a2a304 --- /dev/null +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/common_models/privilege/ProjectDetailListModel.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/review_appointment_page.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +class WaitingAppointmentOnlineCheckinSheet extends StatelessWidget { + WaitingAppointmentOnlineCheckinSheet({super.key, required this.bookAppointmentsViewModel}); + + BookAppointmentsViewModel bookAppointmentsViewModel; + + bool _supportsNFC = false; + + late LocationUtils locationUtils; + late AppState appState; + ProjectDetailListModel projectDetailListModel = ProjectDetailListModel(); + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + locationUtils = getIt.get(); + locationUtils.isShowConfirmDialog = true; + FlutterNfcKit.nfcAvailability.then((value) { + _supportsNFC = (value == NFCAvailability.available); + }); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + checkInOptionCard( + AppAssets.checkin_location_icon, + "Live Location".needTranslation, + "Verify your location to be at hospital to check in".needTranslation, + ).onPress(() { + // locationUtils = LocationUtils( + // isShowConfirmDialog: false, + // navigationService: myAppointmentsViewModel.navigationService, + // appState: myAppointmentsViewModel.appState, + // ); + locationUtils.getCurrentLocation(onSuccess: (value) { + projectDetailListModel = Utils.getProjectDetailObj(appState, bookAppointmentsViewModel.waitingAppointmentProjectID); + double dist = Utils.distance(value.latitude, value.longitude, double.parse(projectDetailListModel.latitude!), double.parse(projectDetailListModel.longitude!)).ceilToDouble() * 1000; + print(dist); + if (dist <= projectDetailListModel.geofenceRadius!) { + checkScannedNFCAndQRCode(projectDetailListModel.checkInQrCode!, context); + } else { + showCommonBottomSheetWithoutHeight(context, + title: "Error".needTranslation, + child: Utils.getErrorWidget(loadingText: "Please ensure you're within the hospital location to perform online check-in.".needTranslation), callBackFunc: () { + Navigator.of(context).pop(); + }, isFullScreen: false); + } + }); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_nfc_icon, + "NFC (Near Field Communication)".needTranslation, + "Scan your phone via NFC board to check in".needTranslation, + ).onPress(() { + Future.delayed(const Duration(milliseconds: 500), () { + showNfcReader(context, onNcfScan: (String nfcId) { + Future.delayed(const Duration(milliseconds: 100), () { + checkScannedNFCAndQRCode(nfcId, context); + }); + }, onCancel: () {}); + }); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_qr_icon, + "QR Code".needTranslation, + "Scan QR code with your camera to check in".needTranslation, + ).onPress(() async { + String onlineCheckInQRCode = (await BarcodeScanner.scan().then((value) => value.rawContent)); + if (onlineCheckInQRCode != "") { + checkScannedNFCAndQRCode(onlineCheckInQRCode, context); + } else {} + }), + ], + ); + } + + Widget checkInOptionCard(String icon, String title, String subTitle) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(isBold: true, color: AppColors.textColor), + subTitle.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ), + ], + ).paddingAll(16.h), + ); + } + + void checkScannedNFCAndQRCode(String scannedCode, BuildContext context) async { + LoaderBottomSheet.showLoader(loadingText: "Processing Check-In...".needTranslation); + bookAppointmentsViewModel.checkScannedNFCAndQRCode( + scannedCode, + bookAppointmentsViewModel.waitingAppointmentProjectID, + onSuccess: (value) { + LoaderBottomSheet.hideLoader(); + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.waitingAppointmentProjectID, clinicID: bookAppointmentsViewModel.waitingAppointmentDoctor!.clinicID!); + bookAppointmentsViewModel.setIsWaitingAppointmentSelected(true); + Navigator.of(context).push( + CustomPageRoute( + page: ReviewAppointmentPage(), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: err), callBackFunc: () { + // Navigator.of(context).pop(); + }, isFullScreen: false); + }, + ); + } +} diff --git a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart new file mode 100644 index 0000000..8cfe6dd --- /dev/null +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart @@ -0,0 +1,667 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class WaitingAppointmentPaymentPage extends StatefulWidget { + WaitingAppointmentPaymentPage({super.key}); + + // PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; + + @override + State createState() => _WaitingAppointmentPaymentPageState(); +} + +class _WaitingAppointmentPaymentPageState extends State { + late BookAppointmentsViewModel bookAppointmentsViewModel; + late MyAppointmentsViewModel myAppointmentsViewModel; + late PayfortViewModel payfortViewModel; + late AppState appState; + + MyInAppBrowser? browser; + String selectedPaymentMethod = ""; + + String transID = ""; + + bool isShowTamara = false; + String tamaraPaymentStatus = ""; + String tamaraOrderID = ""; + + @override + void initState() { + scheduleMicrotask(() { + payfortViewModel.initPayfortViewModel(); + payfortViewModel.setIsApplePayConfigurationLoading(false); + // myAppointmentsViewModel.getPatientShareAppointment( + // widget.patientAppointmentHistoryResponseModel.projectID, + // widget.patientAppointmentHistoryResponseModel.clinicID, + // widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) { + // myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { + // if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + // myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + // setState(() { + // isShowTamara = true; + // }); + // } + // }); + // }, onError: (err) { + // Navigator.of(context).pop(); + // Navigator.of(context).pop(); + // }); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + bookAppointmentsViewModel = Provider.of(context); + myAppointmentsViewModel = Provider.of(context); + payfortViewModel = Provider.of(context); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Appointment Payment".needTranslation, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "MADA"; + openPaymentURL("mada"); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), + ], + ), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "VISA"; + openPaymentURL("visa"); + }), + SizedBox(height: 16.h), + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Tamara".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("tamara"); + }) + : SizedBox.shrink(), + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Consumer(builder: (context, payfortVM, child) { + //TODO: Need to add loading state & animation for Apple Pay Configuration + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.isCash ?? true) + ? Container( + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.secondaryLightRedBorderColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Insurance expired or inactive" + .needTranslation + .toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500) + .paddingSymmetrical(24.h, 0.h), + CustomButton( + text: LocaleKeys.updateInsurance.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.secondaryLightRedBorderColor, + textColor: AppColors.whiteColor, + fontSize: 10.f, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), + ], + ), + ) + : const SizedBox(), + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientTaxAmount! + .toString() + .toText14(isBold: true, color: AppColors.greyTextColor), + AppColors.greyTextColor, + 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString().toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Platform.isIOS + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + // payfortVM.setIsApplePayConfigurationLoading(true); + if (Utils.havePrivilege(103)) { + startApplePay(); + } else { + openPaymentURL("ApplePay"); + } + }) + : SizedBox(height: 12.h), + SizedBox(height: 12.h), + ], + ); + }), + ), + ], + ); + }), + ); + } + + onBrowserLoadStart(String url) { + print("onBrowserLoadStart"); + print(url); + + if (selectedPaymentMethod == "tamara") { + if (Platform.isAndroid) { + Uri uri = new Uri.dataFromString(url); + tamaraPaymentStatus = uri.queryParameters['status']!; + tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!; + } else { + Uri uri = new Uri.dataFromString(url); + tamaraPaymentStatus = uri.queryParameters['paymentStatus']!; + tamaraOrderID = uri.queryParameters['orderId']!; + } + } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.successURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + }); + // } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.errorURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + }); + // } + } + + onBrowserExit(bool isPaymentMade) async { + checkPaymentStatus(); + } + + void checkPaymentStatus() async { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + if (selectedPaymentMethod == "TAMARA") { + await payfortViewModel.checkTamaraPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + if (apiResponse.data["status"].toString().toLowerCase() == "success") { + tamaraOrderID = apiResponse.data["tamara_order_id"].toString(); + await payfortViewModel.updateTamaraRequestStatus( + responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + await payfortViewModel.markAppointmentAsTamaraPaid( + projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, appointmentNo: DateTime.now().millisecondsSinceEpoch); + // await myAppointmentsViewModel.addAdvanceNumberRequest( + // advanceNumber: "Tamara-Advance-0000", + // paymentReference: tamaraOrderID, + // appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + // onSuccess: (value) async { + // if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { + // //TODO: Implement LiveCare Check-In API Call + // await myAppointmentsViewModel.insertLiveCareVIDARequest( + // clientRequestID: tamaraOrderID, + // patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + // onSuccess: (apiResponse) { + // Future.delayed(Duration(milliseconds: 500), () { + // LoaderBottomSheet.hideLoader(); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); + // }); + // }, + // onError: (error) {}); + // } else { + // await myAppointmentsViewModel.generateAppointmentQR( + // clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + // projectID: widget.patientAppointmentHistoryResponseModel.projectID, + // appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + // isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, + // onSuccess: (apiResponse) { + // Future.delayed(Duration(milliseconds: 500), () { + // LoaderBottomSheet.hideLoader(); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); + // }); + // }); + // } + // }); + } else { + await payfortViewModel.updateTamaraRequestStatus( + responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + await payfortViewModel.checkPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + print(apiResponse.data); + if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { + LoaderBottomSheet.hideLoader(); + LoaderBottomSheet.showLoader(loadingText: "Booking Waiting Appointment, Please wait...".needTranslation); + await bookAppointmentsViewModel.insertSpecificAppointmentForWalkIn(onSuccess: (val) async { + String appointmentNo = val.data['AppointmentNo'].toString(); + await myAppointmentsViewModel.createAdvancePayment( + paymentMethodName: selectedPaymentMethod, + projectID: bookAppointmentsViewModel.waitingAppointmentProjectID, + clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!, + appointmentNo: appointmentNo, + payedAmount: payfortViewModel.payfortCheckPaymentStatusResponseModel!.amount!, + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + patientID: appState.getAuthenticatedUser()!.patientId.toString(), + patientType: appState.getAuthenticatedUser()!.patientType!, + onSuccess: (value) async { + print(value); + await myAppointmentsViewModel.addAdvanceNumberRequest( + advanceNumber: Utils.isVidaPlusProject(bookAppointmentsViewModel.waitingAppointmentProjectID) + ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() + : value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(), + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + appointmentNo: appointmentNo, + onSuccess: (value) async { + LoaderBottomSheet.hideLoader(); + sendCheckInRequest(bookAppointmentsViewModel.waitingAppointmentNFCCode, appointmentNo, context); + }); + }); + }); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } + } + + openPaymentURL(String paymentMethod) { + browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context); + transID = Utils.getAppointmentTransID( + bookAppointmentsViewModel.selectedDoctor.projectID!, + bookAppointmentsViewModel.selectedDoctor.clinicID!, + bookAppointmentsViewModel.selectedDoctor.doctorID!, + ); + + //TODO: Need to pass dynamic params to the payment request instead of static values + browser?.openPaymentBrowser( + bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!, + "Appointment check in", + transID, + bookAppointmentsViewModel.selectedDoctor.projectID.toString(), + "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + selectedPaymentMethod, + appState.getAuthenticatedUser()!.patientType.toString(), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!, + browser!, + false, + "2", + "", + context, + "", + "", + "", + "", + "3"); + } + + startApplePay() async { + showCommonBottomSheet(context, + child: Utils.getLoadingWidget(), + callBackFunc: (str) {}, + title: "", + height: ResponsiveExtension.screenHeight * 0.3, + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false); + transID = Utils.getAppointmentTransID( + bookAppointmentsViewModel.selectedDoctor.projectID!, + bookAppointmentsViewModel.selectedDoctor.clinicID!, + bookAppointmentsViewModel.selectedDoctor.doctorID!, + ); + + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); + + await payfortViewModel.getPayfortConfigurations( + serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), + projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, + integrationId: 2); + + applePayInsertRequest.clientRequestID = transID; + applePayInsertRequest.clinicID = bookAppointmentsViewModel.selectedDoctor.clinicID!; + + applePayInsertRequest.currency = appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com"; + applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + + applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken); + applePayInsertRequest.doctorID = bookAppointmentsViewModel.selectedDoctor.doctorID; + applePayInsertRequest.projectID = bookAppointmentsViewModel.selectedDoctor.projectID.toString(); + applePayInsertRequest.serviceID = ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(); + applePayInsertRequest.channelID = 3; + applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType; + applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa; + applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(DateTime.now()); + applePayInsertRequest.appointmentNo = DateTime.now().millisecondsSinceEpoch; + applePayInsertRequest.orderDescription = "Appointment Payment"; + applePayInsertRequest.liveServiceID = "0"; + applePayInsertRequest.latitude = "0.0"; + applePayInsertRequest.longitude = "0.0"; + applePayInsertRequest.amount = bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(); + applePayInsertRequest.isSchedule = "0"; + applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en'; + applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2; + applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId; + applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.paymentOption = "ApplePay"; + + applePayInsertRequest.isMobSDK = true; + applePayInsertRequest.merchantReference = transID; + applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.commandType = "PURCHASE"; + applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.returnURL = ""; + + //TODO: Need to pass dynamic params to the Apple Pay instead of static values + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) { + payfortViewModel.paymentWithApplePay( + customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + // customerEmail: projectViewModel.authenticatedUserObject.user.emailAddress, + customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + orderDescription: "Appointment Payment", + orderAmount: double.parse(bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString()), + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier, + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode, + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest, + currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + log("failureResult: ${failureResult.message.toString()}"); + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + log("successResult: ${successResult.responseMessage.toString()}"); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + checkPaymentStatus(); + }, + // projectId: appo.projectID, + // serviceTypeEnum: ServiceTypeEnum.appointmentPayment, + ); + }); + } + + void sendCheckInRequest(String scannedCode, String appointmentNo, BuildContext context) async { + LoaderBottomSheet.showLoader(loadingText: "Processing Check-In...".needTranslation); + PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel = PatientAppointmentHistoryResponseModel( + appointmentNo: appointmentNo, + clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID, + projectID: bookAppointmentsViewModel.selectedDoctor.projectID, + ); + await myAppointmentsViewModel.sendCheckInNfcRequest( + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + scannedCode: scannedCode, + checkInType: 2, + onSuccess: (apiResponse) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, + title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: apiResponse.data["SuccessMsg"]), callBackFunc: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, isFullScreen: false); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: error), + callBackFunc: () { + Navigator.of(context).pop(); + }, isFullScreen: false); + }, + ); + } + + void insertWalkInAppointment() async {} +} diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index ff966c2..3a9f57d 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; @@ -15,19 +14,19 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/review_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; class AppointmentCalendar extends StatefulWidget { - const AppointmentCalendar({super.key}); + AppointmentCalendar({super.key}); @override State createState() => _AppointmentCalendarState(); @@ -54,7 +53,6 @@ class _AppointmentCalendarState extends State { String selectedTime = ""; - bool isWaitingAppointmentAvailable = false; final _selectedDay = DateTime.now(); @override @@ -111,7 +109,7 @@ class _AppointmentCalendarState extends State { selectionDecoration: ShapeDecoration( color: AppColors.transparent, shape: SmoothRectangleBorder( - borderRadius: BorderRadius.circular(12 ?? 0), + borderRadius: BorderRadius.circular(10.r), smoothness: 1, side: BorderSide(color: AppColors.primaryRedColor, width: 1.5), ), @@ -140,8 +138,9 @@ class _AppointmentCalendarState extends State { }, ), ), + SizedBox(height: 10.h), Transform.translate( - offset: const Offset(0.0, -20.0), + offset: const Offset(0.0, -10.0), child: selectedDateDisplay.toText16(weight: FontWeight.w500), ), //TODO: Add Next Day Span here @@ -175,13 +174,26 @@ class _AppointmentCalendarState extends State { text: "Select".needTranslation, onPressed: () async { if (appState.isAuthenticated) { - bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime); - Navigator.of(context).pop(); - Navigator.of(context).push( - CustomPageRoute( - page: ReviewAppointmentPage(), - ), - ); + if(selectedTime == "Waiting Appointment".needTranslation){ + bookAppointmentsViewModel.setWaitingAppointmentProjectID(bookAppointmentsViewModel.selectedDoctor.projectID!); + bookAppointmentsViewModel.setWaitingAppointmentDoctor(bookAppointmentsViewModel.selectedDoctor); + + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: WaitingAppointmentInfo(), + ), + ); + } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); + bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime); + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: ReviewAppointmentPage(), + ), + ); + } } else { showCommonBottomSheetWithoutHeight( context, @@ -246,10 +258,10 @@ class _AppointmentCalendarState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + borderRadius: 10.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), height: 50.h, ), ], @@ -280,8 +292,8 @@ class _AppointmentCalendarState extends State { openTimeSlotsPickerForDate(DateTime dateStart, List freeSlots) { dayEvents.clear(); DateTime dateStartObj = new DateTime(dateStart.year, dateStart.month, dateStart.day, 0, 0, 0, 0, 0); - if (isWaitingAppointmentAvailable && DateUtils.isSameDay(dateStart, DateTime.now())) { - dayEvents.add(TimeSlot(isoTime: "Waiting Appointment", start: DateTime.now(), end: DateTime.now(), vidaDate: "")); + if (bookAppointmentsViewModel.isWaitingAppointmentAvailable && DateUtils.isSameDay(dateStart, DateTime.now())) { + dayEvents.add(TimeSlot(isoTime: "Waiting Appointment".needTranslation, start: DateTime.now(), end: DateTime.now(), vidaDate: "")); } freeSlots.forEach((v) { if (v.start == dateStartObj) dayEvents.add(v); @@ -320,9 +332,25 @@ class TimeSlotChip extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onTap, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 14.h, vertical: 8.h), - decoration: ShapeDecoration( + child: label == "Waiting Appointment".needTranslation + ? Container( + padding: EdgeInsets.symmetric(horizontal: 14.h, vertical: 8.h), + decoration: ShapeDecoration( + color: isSelected ? AppColors.warningColorYellow : AppColors.whiteColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8.h), + smoothness: 1, + side: BorderSide(color: isSelected ? AppColors.warningColorYellow : AppColors.borderOnlyColor.withOpacity(0.2), width: 1), + ), + ), + child: label.toText12( + color: isSelected ? AppColors.whiteColor : Colors.black87, + fontWeight: FontWeight.w500, + ), + ) + : Container( + padding: EdgeInsets.symmetric(horizontal: 14.h, vertical: 8.h), + decoration: ShapeDecoration( color: AppColors.whiteColor, shape: SmoothRectangleBorder( borderRadius: BorderRadius.circular(8.h), diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 3f3d51c..ffe26ff 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -16,11 +17,17 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; class DoctorCard extends StatelessWidget { - DoctorCard({super.key, required this.doctorsListResponseModel, required this.isLoading, required this.bookAppointmentsViewModel}); + const DoctorCard({ + super.key, + required this.doctorsListResponseModel, + required this.isLoading, + required this.bookAppointmentsViewModel, + }); - DoctorsListResponseModel doctorsListResponseModel; - bool isLoading = false; - BookAppointmentsViewModel bookAppointmentsViewModel; + final DoctorsListResponseModel doctorsListResponseModel; + final bool isLoading; + + final BookAppointmentsViewModel bookAppointmentsViewModel; @override Widget build(BuildContext context) { @@ -43,7 +50,7 @@ class DoctorCard extends StatelessWidget { : doctorsListResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png", width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: isLoading), SizedBox(width: 8.h), Expanded( @@ -55,8 +62,24 @@ class DoctorCard extends StatelessWidget { children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.49, - child: (isLoading ? "Dr John Smith" : "${doctorsListResponseModel.doctorTitle} ${doctorsListResponseModel.name}").toString().toText16(isBold: true, maxlines: 1), + child: (isLoading ? "Dr John Smith" : "${doctorsListResponseModel.doctorTitle} ${doctorsListResponseModel.name}") + .toString() + .toText16(isBold: true, maxlines: 1), ).toShimmer2(isShow: isLoading), + ], + ), + SizedBox(height: 2.h), + Row( + children: [ + (isLoading + ? "Consultant Cardiologist" + : doctorsListResponseModel.speciality!.isNotEmpty + ? doctorsListResponseModel.speciality!.first + : "") + .toString() + .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1) + .toShimmer2(isShow: isLoading), + SizedBox(width: 6.w), Image.network( isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png", width: 20.h, @@ -65,21 +88,13 @@ class DoctorCard extends StatelessWidget { ).toShimmer2(isShow: isLoading), ], ), - SizedBox(height: 2.h), - (isLoading - ? "Consultant Cardiologist" - : doctorsListResponseModel.speciality!.isNotEmpty - ? doctorsListResponseModel.speciality!.first - : "") - .toString() - .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1) - .toShimmer2(isShow: isLoading), ], ), ), Expanded( flex: 1, - child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).toShimmer2(isShow: isLoading), + child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown) + .toShimmer2(isShow: isLoading), ), ], ), @@ -95,11 +110,20 @@ class DoctorCard extends StatelessWidget { AppCustomChipWidget( labelText: "Branch: ${isLoading ? "Olaya Hospital" : doctorsListResponseModel.projectName}".needTranslation, ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( + doctorsListResponseModel.decimalDoctorRate != null ? AppCustomChipWidget( icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, labelText: "Rating: ${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".needTranslation, - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: isLoading) : SizedBox(), + bookAppointmentsViewModel.isNearestAppointmentSelected + ? doctorsListResponseModel.nearestFreeSlot != null + ? AppCustomChipWidget( + labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)).needTranslation, + backgroundColor: AppColors.successColor, + textColor: AppColors.whiteColor, + ).toShimmer2(isShow: isLoading) + : SizedBox.shrink() + : SizedBox.shrink(), ], ), SizedBox(height: 12.h), @@ -159,10 +183,10 @@ class DoctorCard extends StatelessWidget { backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), textColor: Color(0xffED1C2B), - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), height: 40.h, icon: AppAssets.add_icon, iconColor: AppColors.primaryRedColor, diff --git a/lib/presentation/comprehensive_checkup/cmc_order_detail_page.dart b/lib/presentation/comprehensive_checkup/cmc_order_detail_page.dart new file mode 100644 index 0000000..7547fd0 --- /dev/null +++ b/lib/presentation/comprehensive_checkup/cmc_order_detail_page.dart @@ -0,0 +1,255 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class CmcOrderDetailPage extends StatefulWidget { + const CmcOrderDetailPage({super.key}); + + @override + State createState() => _CmcOrderDetailPageState(); +} + +class _CmcOrderDetailPageState extends State { + @override + void initState() { + super.initState(); + final hmgServicesViewModel = context.read(); + scheduleMicrotask(() async { + await hmgServicesViewModel.getCmcOrdersList(); + }); + } + + Color _getStatusColor(int? statusId) { + switch (statusId) { + case 1: // Pending + return const Color(0xffCC9B14); + case 2: // Processing + return const Color(0xff2E303A); + case 3: // Completed + return const Color(0xff359846); + case 4: // Cancelled + case 6: // Rejected + case 7: // Rejected + return const Color(0xffD02127); + default: + return AppColors.greyColor; + } + } + + String _formatDate(String? dateString) { + if (dateString == null) return ''; + try { + final date = DateTime.parse(dateString); + return DateFormat('MMM dd, yyyy').format(date); + } catch (e) { + return dateString; + } + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return _buildOrderCard(GetCMCAllOrdersResponseModel(), isLoading: true); + }, + ); + } + + Widget _buildOrderCard(GetCMCAllOrdersResponseModel order, {bool isLoading = false}) { + final statusColor = _getStatusColor(order.statusId); + final canCancel = order.statusId == 1 || order.statusId == 2; + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and Date Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: (isLoading ? "Processing" : order.statusText ?? '') + .toText12( + color: statusColor, + fontWeight: FontWeight.w600, + ) + .toShimmer2(isShow: isLoading), + ), + SizedBox(width: 8.w), + (isLoading ? "Jan 15, 2024" : _formatDate(order.created)) + .toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w500, + ) + .toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 16.h), + + // Request ID + Row( + children: [ + if (!isLoading) ...[ + "Request ID:".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ], + (isLoading ? "12345" : "${order.iD ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Chips for Hospital, Service, and Amount + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + // Hospital + if (order.projectName != null || isLoading) + AppCustomChipWidget( + icon: AppAssets.hospital, + labelText: isLoading ? "Hospital Name" : order.projectName ?? '-', + ).toShimmer2(isShow: isLoading), + + // Service + if (order.serviceText != null || isLoading) + AppCustomChipWidget( + icon: AppAssets.servicesBottom, + labelText: isLoading ? "Service Name" : order.serviceText ?? '-', + ).toShimmer2(isShow: isLoading), + ], + ), + + // Cancel Button + if (canCancel || isLoading) ...[ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Cancel Order".needTranslation, + onPressed: isLoading ? () {} : () => CmcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 10.r, + height: 44.h, + ).toShimmer2(isShow: isLoading), + ), + ], + ), + ] + ], + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any CMC orders yet.".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "CMC Orders".needTranslation, + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, viewModel, child) { + if (viewModel.isCmcOrdersLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.cmcOrdersList.isEmpty) { + return _buildEmptyState(); + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.cmcOrdersList.length, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final order = viewModel.cmcOrdersList.reversed.toList()[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: _buildOrderCard(order), + ), + ), + ); + }, + ); + }, + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart new file mode 100644 index 0000000..18b656c --- /dev/null +++ b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart @@ -0,0 +1,439 @@ +import 'dart:developer'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/map/location_map_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + +class CmcSelectionReviewPage extends StatefulWidget { + final GetCMCServicesResponseModel selectedService; + final HospitalsModel? preSelectedHospital; + + const CmcSelectionReviewPage({super.key, required this.selectedService, this.preSelectedHospital}); + + @override + State createState() => _CmcSelectionReviewPageState(); +} + +class _CmcSelectionReviewPageState extends State { + @override + void initState() { + super.initState(); + // Initialize ViewModel state with preselected hospital if provided + if (widget.preSelectedHospital != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final hmgServicesViewModel = context.read(); + hmgServicesViewModel.setSelectedHospitalForOrder(widget.preSelectedHospital); + hmgServicesViewModel.setSelectedServiceForOrder(widget.selectedService); + }); + } + } + + @override + Widget build(BuildContext context) { + final appState = getIt.get(); + final isArabic = appState.isArabic(); + + return CollapsingListView( + title: "Summary".needTranslation, + bottomChild: _buildBottomButton(), + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildOrderSummaryCard(isArabic), + SizedBox(height: 16.h), + _buildSelectedServiceCard(isArabic), + SizedBox(height: 16.h), + _buildPaymentSummary(), + ], + ), + ), + ); + } + + Widget _buildOrderSummaryCard(bool isArabic) { + return Consumer( + builder: (context, hmgServicesViewModel, child) { + final selectedHospital = hmgServicesViewModel.selectedHospitalForOrder; + final isLocationSelected = selectedHospital != null; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + ), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select Hospital".needTranslation, + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w700, + color: AppColors.blackColor, + letterSpacing: -0.5, + ), + ), + SizedBox(height: 12.h), + _buildHospitalSelector(isArabic, selectedHospital, isLocationSelected), + if (isLocationSelected) ...[ + SizedBox(height: 16.h), + _buildHospitalMap(selectedHospital), + ], + ], + ), + ); + }, + ); + } + + Widget _buildHospitalSelector(bool isArabic, HospitalsModel? selectedHospital, bool isLocationSelected) { + return InkWell( + onTap: _showHospitalSelectionBottomSheet, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: AppColors.greyColor.withAlpha(51), + width: 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isLocationSelected && selectedHospital != null + ? (isArabic ? (selectedHospital.nameN ?? selectedHospital.name ?? '') : (selectedHospital.name ?? '')) + : "Select Hospital".needTranslation, + style: TextStyle( + fontSize: 14.f, + fontWeight: isLocationSelected ? FontWeight.w600 : FontWeight.w400, + color: isLocationSelected ? AppColors.blackColor : AppColors.greyTextColor, + letterSpacing: -0.4, + ), + ), + ], + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: AppColors.greyTextColor, + size: 24.h, + ), + ], + ), + ), + ); + } + + Widget _buildHospitalMap(HospitalsModel selectedHospital) { + final String lat = ((selectedHospital.latitude == null || selectedHospital.latitude!.isEmpty) ? "0.0" : selectedHospital.latitude) ?? "0.0"; + final String lng = ((selectedHospital.longitude == null || selectedHospital.longitude!.isEmpty) ? "0.0" : selectedHospital.longitude) ?? "0.0"; + + log("selectedHospital: $lng and $lat"); + + if (lat == "0.0" || lng == "0.0") return SizedBox.shrink(); + + final appState = getIt.get(); + final isArabic = appState.isArabic(); + final hospitalName = isArabic ? (selectedHospital.nameN ?? selectedHospital.name ?? '') : (selectedHospital.name ?? ''); + + return LocationMapWidget( + latitude: double.parse(lat), + longitude: double.parse(lng), + address: hospitalName, + showAddress: false, + padding: EdgeInsets.zero, + onDirectionsTap: () => _launchDirections(selectedHospital), + ); + } + + Widget _buildSelectedServiceCard(bool isArabic) { + final serviceName = isArabic ? (widget.selectedService.textN ?? widget.selectedService.text ?? '') : (widget.selectedService.text ?? ''); + final price = widget.selectedService.priceTotal ?? 0.0; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + ), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Selected Service".needTranslation.toText14( + weight: FontWeight.w600, + color: AppColors.greyTextColor, + letterSpacing: -0.4, + ), + SizedBox(height: 6.h), + Row( + children: [ + Expanded(child: serviceName.toText16(weight: FontWeight.w700, color: AppColors.blackColor, letterSpacing: -0.5)), + ], + ), + ], + ), + ); + } + + Widget _buildPaymentSummary() { + // Use selected service from widget + final service = widget.selectedService; + + log("service: ${service.toJson()}"); + + final double amountBeforeTax = service.price ?? 0.0; + final double taxAmount = service.priceVAT ?? 0.0; + final double totalAmount = service.priceTotal ?? (amountBeforeTax + taxAmount); + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Consumer(builder: (context, payfortVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + + // Amount before tax + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + amountBeforeTax.toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + + // VAT (use label VAT 15% if desired) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + taxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor), + AppColors.greyTextColor, + 13, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + // Total Amount + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + totalAmount.toString().toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + + SizedBox(height: 16.h), + ], + ); + }), + ); + } + + Widget _buildBottomButton() { + return Consumer( + builder: (context, hmgServicesViewModel, child) { + final isLocationSelected = hmgServicesViewModel.selectedHospitalForOrder != null; + + return SafeArea( + top: false, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + boxShadow: [ + BoxShadow( + color: Color.fromARGB(13, 0, 0, 0), + blurRadius: 8, + offset: Offset(0, -2), + ), + ], + ), + child: CustomButton( + text: "Confirm".needTranslation, + onPressed: () { + isLocationSelected ? _handleConfirm() : null; + }, + textColor: AppColors.whiteColor, + backgroundColor: isLocationSelected ? AppColors.successColor : AppColors.greyColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + borderWidth: 0, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ); + } + + void _showHospitalSelectionBottomSheet() { + CmcUiSelectionHelper.showHospitalSelectionBottomSheet(context: context, onHospitalSelected: (hospital) => context.pop()); + } + + void _launchDirections(HospitalsModel selectedHospital) { + final double lat = double.parse(selectedHospital.latitude ?? "0.0"); + final double lng = double.parse(selectedHospital.longitude ?? "0.0"); + + if (lat != 0.0 && lng != 0.0) { + MapsLauncher.launchCoordinates( + lat, + lng, + selectedHospital.name ?? "Hospital", + ); + } + } + + showSuccessBottomSheet(int requestId, HmgServicesViewModel hmgServicesViewModel) { + return showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: "Your request has been successfully submitted.".needTranslation), + Row( + children: [ + "Here is your request #: ".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ("$requestId").toText16(isBold: true), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + context.pop(); + context.pop(); + hmgServicesViewModel.getAllCmcOrders(); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + } + + void _handleConfirm() { + final hmgServicesViewModel = context.read(); + final selectedHospital = hmgServicesViewModel.selectedHospitalForOrder; + + if (selectedHospital == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Please select a hospital to continue".needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + return; + } + + final selectedService = widget.selectedService; + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to submit this request?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + // Create the services list + final servicesList = [ + PatientERCMCInsertServicesList( + recordID: selectedService.iD, + serviceID: selectedService.serviceID, + selectedServiceName: selectedService.text, + selectedServiceNameAR: selectedService.textN, + price: selectedService.price, + vAT: selectedService.priceVAT, + totalPrice: selectedService.priceTotal, + ), + ]; + + await hmgServicesViewModel.addCmcOrder( + projectID: selectedHospital.mainProjectID ?? 0, + orderServiceID: selectedService.orderServiceID ?? 3, + services: servicesList, + onSuccess: (requestId) { + LoaderBottomSheet.hideLoader(); + showSuccessBottomSheet(requestId, hmgServicesViewModel); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + // showCommonBottomSheetWithoutHeight(context, child: Utils.getErrorWidget(loadingText: err), callBackFunc: () {}); + }, + ); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } +} diff --git a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart new file mode 100644 index 0000000..8b9ad89 --- /dev/null +++ b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart @@ -0,0 +1,410 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/cmc_order_detail_page.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/cmc_selection_review_page.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/media_viewer/full_screen_image_viewer.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class ComprehensiveCheckupPage extends StatefulWidget { + const ComprehensiveCheckupPage({super.key}); + + @override + State createState() => _ComprehensiveCheckupPageState(); +} + +class _ComprehensiveCheckupPageState extends State { + int? _selectedServiceId; + GetCMCServicesResponseModel? _selectedService; + + @override + void initState() { + super.initState(); + final HmgServicesViewModel hmgServicesViewModel = context.read(); + final AppState appState = getIt.get(); + + scheduleMicrotask(() async { + final user = appState.getAuthenticatedUser(); + if (user != null) { + await hmgServicesViewModel.getAllCmcOrders(); + await hmgServicesViewModel.getAllCmcServices(patientID: user.patientId ?? 0); + } + }); + } + + GetCMCAllOrdersResponseModel? _getPendingOrder(List orders) { + if (orders.isEmpty) return null; + + // Find pending or processing orders (status 1 or 2) + for (var order in orders) { + if (order.statusId == 1 || order.statusId == 2) { + return order; + } + } + + return null; + } + + Widget _buildPendingOrderCard(GetCMCAllOrdersResponseModel order) { + int status = order.statusId ?? 0; + String statusDisp = order.statusText ?? ""; + Color statusColor; + + if (status == 1) { + // pending + statusColor = AppColors.statusPendingColor; + } else if (status == 2) { + // processing + statusColor = AppColors.statusProcessingColor; + } else if (status == 3) { + // completed + statusColor = AppColors.statusCompletedColor; + } else { + // cancel / rejected + statusColor = AppColors.statusRejectedColor; + } + + final canCancel = order.statusId == 1 || order.statusId == 2; + + return Container( + width: double.infinity, + margin: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and Date Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: statusDisp.toText12( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8.w), + if (order.created != null) + DateFormat('MMM dd, yyyy').format(DateTime.parse(order.created!)).toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w500, + ), + ], + ), + + SizedBox(height: 16.h), + + // Request ID + Row( + children: [ + "Request ID:".needTranslation.toText14(color: AppColors.textColorLight, weight: FontWeight.w500), + SizedBox(width: 4.w), + "${order.iD ?? '-'}".toText16(isBold: true), + ], + ), + + SizedBox(height: 12.h), + + // Chips for Hospital, Service, and Amount + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + // Hospital + if (order.projectName != null) + AppCustomChipWidget( + icon: AppAssets.hospital, + labelText: order.projectName ?? '-', + ), + + // Service + if (order.serviceText != null) + AppCustomChipWidget( + icon: AppAssets.file_icon, + labelText: order.serviceText ?? '-', + ), + ], + ), + + SizedBox(height: 16.h), + + // Info message + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: AppColors.infoBannerBgColor, + borderRadius: BorderRadius.circular(10.r), + border: Border.all( + color: AppColors.infoBannerBorderColor, + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 20.w, + color: AppColors.infoBannerIconColor, + ), + SizedBox(width: 8.w), + Expanded( + child: "You have a pending order. Please wait for it to be processed.".needTranslation.toText12( + color: AppColors.infoBannerTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + if (canCancel) ...[ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Cancel Order".needTranslation, + onPressed: () => CmcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 10.r, + height: 44.h, + ), + ), + ], + ), + ] + ], + ), + ), + ); + } + + Widget _buildServiceSelectionList(List services) { + if (services.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Text( + 'No services available'.needTranslation, + style: TextStyle( + fontSize: 16.h, + color: AppColors.greyTextColor, + ), + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + Text( + 'Select a Service'.needTranslation, + style: TextStyle( + fontSize: 20.h, + fontWeight: FontWeight.w700, + color: AppColors.blackColor, + letterSpacing: -0.8, + ), + ).paddingOnly(left: 16.w, right: 16.w), + ListView.builder( + padding: EdgeInsets.zero, + itemCount: services.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final service = services[index]; + final serviceName = service.text ?? service.textN ?? ''; + final price = service.priceTotal ?? 0.0; + return RadioListTileWidget( + value: service.iD ?? 0, + groupValue: _selectedServiceId, + title: serviceName, + subtitleWidget: Utils.getPaymentAmountWithSymbol( + isExpanded: false, + price.toString().toText14(), + AppColors.blackColor, + 14, + isSaudiCurrency: true, + ), + onChanged: (value) { + setState(() { + _selectedServiceId = value; + _selectedService = service; + }); + }, + ); + }, + ), + // Illustration image below the services list similar to the old implementation + SizedBox(height: 12.h), + Builder(builder: (context) { + final appStateLocal = getIt.get(); + final String imagePath = appStateLocal.isArabic() ? AppAssets.comprehensiveCheckupAr : AppAssets.comprehensiveCheckupEn; + return Stack( + children: [ + Image.asset( + imagePath, + width: double.infinity, + fit: BoxFit.cover, + ).paddingAll(16.w), + Align( + alignment: Alignment.topRight, + child: Container( + decoration: BoxDecoration( + color: Color.fromARGB(51, 0, 0, 0), + borderRadius: BorderRadius.circular(1000.r), + ), + margin: EdgeInsets.all(16.h), + child: IconButton( + icon: Icon( + Icons.zoom_in, + color: Colors.white, + size: 26.w, + ), + padding: EdgeInsets.all(10.h), + onPressed: () => _showFullScreenImage(context, imagePath, isSvg: false), + ), + ), + ), + ], + ); + }), + ], + ); + } + + void _proceedWithSelectedService() { + if (_selectedService != null) { + final hmgServicesViewModel = context.read(); + + // Store selected service in ViewModel + hmgServicesViewModel.setSelectedServiceForOrder(_selectedService); + hmgServicesViewModel.getHospitalsList(); + // Show hospital selection bottom sheet using common helper + CmcUiSelectionHelper.showHospitalSelectionBottomSheet( + context: context, + onHospitalSelected: (hospital) { + Navigator.of(context).pushReplacement( + CustomPageRoute( + page: CmcSelectionReviewPage(selectedService: _selectedService!, preSelectedHospital: hospital), + direction: AxisDirection.left, + ), + ); + }, + ); + } + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(16.w), + itemCount: 10, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Comprehensive Checkup".needTranslation, + history: () => Navigator.of(context).push(CustomPageRoute(page: CmcOrderDetailPage(), direction: AxisDirection.up)), + bottomChild: Consumer( + builder: (context, hmgServicesViewModel, child) { + if (hmgServicesViewModel.isCmcOrdersLoading || hmgServicesViewModel.isCmcServicesLoading) return SizedBox.shrink(); + final pendingOrder = _getPendingOrder(hmgServicesViewModel.cmcOrdersList); + if (pendingOrder == null && _selectedServiceId != null) { + return SafeArea( + top: false, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h), + child: CustomButton( + borderWidth: 0, + text: "Next".needTranslation, + onPressed: _proceedWithSelectedService, + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + } + return SizedBox.shrink(); + }, + ), + child: Consumer( + builder: (context, hmgServicesViewModel, child) { + if (hmgServicesViewModel.isCmcOrdersLoading || hmgServicesViewModel.isCmcServicesLoading) { + return _buildLoadingShimmer(); + } + final pendingOrder = _getPendingOrder(hmgServicesViewModel.cmcOrdersList); + if (pendingOrder != null) { + return _buildPendingOrderCard(pendingOrder); + } else { + return _buildServiceSelectionList(hmgServicesViewModel.cmcServicesList); + } + }, + ), + ); + } + + void _showFullScreenImage(BuildContext context, String path, {bool isSvg = false}) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => FullScreenImageViewer(isSvg: isSvg, path: path))); + } +} diff --git a/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_bottom_sheet_body.dart b/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_bottom_sheet_body.dart new file mode 100644 index 0000000..98e91b8 --- /dev/null +++ b/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_bottom_sheet_body.dart @@ -0,0 +1,111 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.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/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_hospital_list_item.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class CmcHospitalBottomSheetBody extends StatelessWidget { + final Function(HospitalsModel) onHospitalSelected; + + const CmcHospitalBottomSheetBody({super.key, required this.onHospitalSelected}); + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final appState = getIt.get(); + final bool isArabic = appState.isArabic(); + final bool isLocationEnabled = (appState.userLat != 0) && (appState.userLong != 0); + + return Consumer( + builder: (BuildContext context, HmgServicesViewModel hmgServicesViewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Choose your preferred hospital for the service".needTranslation.toText14( + weight: FontWeight.w400, + color: AppColors.greyTextColor, + letterSpacing: -0.4, + ), + SizedBox(height: 16.h), + TextInputWidget( + labelText: LocaleKeys.search.tr(), + hintText: LocaleKeys.searchHospital.tr(), + onChange: (value) { + hmgServicesViewModel.filterHospitalsByString(value ?? '', isArabic); + }, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + isAllowLeadingIcon: true, + selectionType: SelectionTypeEnum.search, + padding: EdgeInsets.symmetric(vertical: ResponsiveExtension(10).h, horizontal: ResponsiveExtension(15).h), + ), + ], + ), + SizedBox(height: 8.h), + SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: hmgServicesViewModel.isHospitalListLoading + ? _buildLoadingShimmer() + : hmgServicesViewModel.filteredHospitalsList.isEmpty + ? Center( + child: "No hospitals Found".needTranslation.toText16(weight: FontWeight.w500, color: AppColors.greyTextColor), + ) + : ListView.separated( + itemCount: hmgServicesViewModel.filteredHospitalsList.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final hospital = hmgServicesViewModel.filteredHospitalsList[index]; + return CmcHospitalListItem( + hospital: hospital, + isLocationEnabled: isLocationEnabled, + onPress: () { + hmgServicesViewModel.setSelectedHospital(hospital); + onHospitalSelected(hospital); + }, + ); + }, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_list_item.dart b/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_list_item.dart new file mode 100644 index 0000000..39d6d7c --- /dev/null +++ b/lib/presentation/comprehensive_checkup/widgets/cmc_hospital_list_item.dart @@ -0,0 +1,126 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class CmcHospitalListItem extends StatelessWidget { + final HospitalsModel hospital; + final VoidCallback onPress; + final bool isLocationEnabled; + + const CmcHospitalListItem({ + super.key, + required this.hospital, + required this.onPress, + this.isLocationEnabled = false, + }); + + @override + Widget build(BuildContext context) { + final appState = getIt.get(); + final bool isArabic = appState.isArabic(); + final String hospitalName = isArabic ? (hospital.nameN ?? hospital.name ?? '') : (hospital.name ?? ''); + + return InkWell( + onTap: onPress, + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + _buildHospitalName(hospitalName), + _buildDistanceInfo(), + ], + ), + ), + Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ), + ); + } + + Widget _buildHospitalName(String hospitalName) { + return Row( + children: [ + Utils.buildSvgWithAssets( + icon: (hospital.isHMC == true) ? AppAssets.hmc : AppAssets.hmg, + ).paddingOnly(right: 10), + Expanded( + child: Text( + hospitalName, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: AppColors.blackColor, + ), + ), + ) + ], + ); + } + + Widget _buildDistanceInfo() { + log("hospital: ${hospital.distanceInKilometers}"); + final distanceText = hospital.distanceInKilometers != null ? hospital.distanceInKilometers!.toStringAsFixed(1) : "0"; + + return Row( + spacing: 4.w, + children: [ + Visibility( + visible: (hospital.distanceInKilometers != null && hospital.distanceInKilometers! > 0), + child: AppCustomChipWidget( + labelText: "$distanceText km".needTranslation, + icon: AppAssets.location_red, + iconColor: AppColors.errorColor, + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + Visibility( + visible: (hospital.distanceInKilometers == null || hospital.distanceInKilometers == 0), + child: AppCustomChipWidget( + labelText: " Distance not available".needTranslation, + textColor: AppColors.blackColor, + ), + ), + // Visibility( + // visible: !isLocationEnabled, + // child: AppCustomChipWidget( + // labelText: "Location turned off".needTranslation, + // deleteIcon: AppAssets.location_unavailable, + // deleteIconSize: Size(9.w, 12.h), + // textColor: AppColors.blackColor, + // ), + // ), + ], + ); + } +} diff --git a/lib/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.dart b/lib/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.dart new file mode 100644 index 0000000..908aac9 --- /dev/null +++ b/lib/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_hospital_bottom_sheet_body.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/order_update_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; + +class CmcUiSelectionHelper { + static void showHospitalSelectionBottomSheet({ + required BuildContext context, + required Function(dynamic) onHospitalSelected, + }) { + final hmgServicesViewModel = context.read(); + + showCommonBottomSheetWithoutHeight( + context, + title: "Select Hospital".needTranslation, + child: CmcHospitalBottomSheetBody( + onHospitalSelected: (hospital) { + hmgServicesViewModel.setSelectedHospitalForOrder(hospital); + onHospitalSelected(hospital); + }, + ), + ); + } + + static void showCancelConfirmationDialog({ + required BuildContext context, + required GetCMCAllOrdersResponseModel order, + }) { + final HmgServicesViewModel hmgServicesViewModel = context.read(); + + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to cancel this order?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + final requestModel = OrderUpdateRequestModel( + presOrderID: order.iD, + rejectionReason: "Cancelled by user", + presOrderStatus: 4, // Cancelled status + editedBy: 3, + ); + + await hmgServicesViewModel.updateCmcPresOrder( + requestModel: requestModel, + onSuccess: (_) async { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: "Order has been cancelled successfully".needTranslation), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + context.pop(); + context.pop(); + hmgServicesViewModel.getAllCmcOrders(); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + }, + ); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } +} diff --git a/lib/presentation/contact_us/contact_us.dart b/lib/presentation/contact_us/contact_us.dart new file mode 100644 index 0000000..d7ea9c5 --- /dev/null +++ b/lib/presentation/contact_us/contact_us.dart @@ -0,0 +1,127 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/find_us_page.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/live_chat_page.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class ContactUs extends StatelessWidget { + ContactUs({super.key}); + + late AppState appState; + late ContactUsViewModel contactUsViewModel; + late LocationUtils locationUtils; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + locationUtils = getIt.get(); + locationUtils.isShowConfirmDialog = true; + contactUsViewModel = Provider.of(context); + return Column( + children: [ + checkInOptionCard( + AppAssets.checkin_location_icon, + LocaleKeys.findUs.tr(), + "View your nearest HMG locations".needTranslation, + ).onPress(() { + locationUtils.getCurrentLocation(onSuccess: (value) { + contactUsViewModel.initContactUsViewModel(); + Navigator.pop(context); + Navigator.of(context).push( + CustomPageRoute( + page: FindUsPage(), + ), + ); + }); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + LocaleKeys.feedback.tr(), + "Provide your feedback on our services".needTranslation, + ).onPress(() { + contactUsViewModel.setSelectedFeedbackType( + FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد'), + ); + Navigator.pop(context); + Navigator.of(context).push( + CustomPageRoute( + page: FeedbackPage(), + ), + ); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + LocaleKeys.liveChat.tr(), + "Live chat option with HMG".needTranslation, + ).onPress(() { + locationUtils.getCurrentLocation(onSuccess: (value) { + contactUsViewModel.getLiveChatProjectsList(); + Navigator.pop(context); + Navigator.of(context).push( + CustomPageRoute( + page: LiveChatPage(), + ), + ); + }); + }), + ], + ); + } + + Widget checkInOptionCard(String icon, String title, String subTitle) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(isBold: true, color: AppColors.textColor), + subTitle.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ), + ], + ).paddingAll(16.h), + ); + } +} diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart new file mode 100644 index 0000000..078b3a1 --- /dev/null +++ b/lib/presentation/contact_us/feedback_page.dart @@ -0,0 +1,397 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/widgets/feedback_appointment_selection.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class FeedbackPage extends StatelessWidget { + FeedbackPage({super.key}); + + late ContactUsViewModel contactUsViewModel; + late MedicalFileViewModel medicalFileViewModel; + + final TextEditingController subjectTextController = TextEditingController(); + final TextEditingController messageTextController = TextEditingController(); + + @override + Widget build(BuildContext context) { + contactUsViewModel = Provider.of(context, listen: false); + medicalFileViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, contactUsVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + isLeading: Navigator.canPop(context), + title: LocaleKeys.feedback.tr(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + CustomTabBar( + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "Send".needTranslation), + CustomTabBarModel(null, "Status".needTranslation), + ], + onTabChange: (index) { + contactUsViewModel.setIsSendFeedbackTabSelected(index == 0); + }, + ).paddingSymmetrical(24.h, 0.h), + getSelectedTabWidget(context).paddingSymmetrical(24.h, 16.w), + ], + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + hasShadow: true, + ), + child: CustomButton( + text: LocaleKeys.submit.tr(context: context), + onPressed: () async { + if (subjectTextController.text.isEmpty) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.emptySubject.tr(context: context)), + ); + return; + } + if (messageTextController.text.isEmpty) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.emptyMessage.tr(context: context)), + ); + return; + } + LoaderBottomSheet.showLoader(loadingText: "Sending Feedback...".needTranslation); + contactUsViewModel.insertCOCItem( + subject: subjectTextController.text, + message: messageTextController.text, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + subjectTextController.clear(); + messageTextController.clear(); + contactUsViewModel.setPatientFeedbackSelectedAppointment(null); + showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr(context: context)), callBackFunc: () { + Navigator.pop(context); + }); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: err), + ); + }); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.feedback, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ); + }), + ); + } + + Widget getSelectedTabWidget(BuildContext context) { + if (contactUsViewModel.isSendFeedbackTabSelected) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.likeToHear.tr().toText14(weight: FontWeight.w500), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ask_doctor_icon, width: 24.w, height: 24.h, iconColor: AppColors.greyTextColor), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.feedbackType.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), + (getIt.get().isArabic() ? contactUsViewModel.selectedFeedbackType.nameAR : contactUsViewModel.selectedFeedbackType.nameEN) + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, + title: "Select Feedback Type".needTranslation, + child: Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: ListView.builder( + itemCount: contactUsViewModel.feedbackTypeList.length, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(top: 8, bottom: 8), + shrinkWrap: true, + itemBuilder: (innerContext, index) { + return Theme( + data: Theme.of(context).copyWith( + listTileTheme: ListTileThemeData(horizontalTitleGap: 4), + ), + child: RadioListTile( + title: Text( + getIt.get().isArabic() ? contactUsViewModel.feedbackTypeList[index].nameAR : contactUsViewModel.feedbackTypeList[index].nameEN, + style: TextStyle( + fontSize: 16.h, + fontWeight: FontWeight.w500, + ), + ), + value: contactUsViewModel.feedbackTypeList[index], + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppColors.primaryRedColor; + } + return Color(0xffEEEEEE); + }), + contentPadding: EdgeInsets.only(left: 12.h, right: 12.h), + groupValue: contactUsViewModel.selectedFeedbackType, + onChanged: (FeedbackType? newValue) async { + Navigator.pop(context); + contactUsViewModel.setSelectedFeedbackType(newValue!); + if (contactUsViewModel.selectedFeedbackType.id == 1) { + LoaderBottomSheet.showLoader(loadingText: "Loading appointments list...".needTranslation); + await medicalFileViewModel.getPatientMedicalReportAppointmentsList(onSuccess: (val) async { + LoaderBottomSheet.hideLoader(); + bool? value = await Navigator.of(context).push( + CustomPageRoute( + page: FeedbackAppointmentSelection(), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + if (value != null) { + // showConfirmRequestMedicalReportBottomSheet(); + } + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "You do not have any appointments to submit a feedback.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + contactUsViewModel.setPatientFeedbackSelectedAppointment(null); + } + }, + ), + ); + }, + ), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true); + }), + ]), + ), + ), + if (contactUsViewModel.patientFeedbackSelectedAppointment != null) ...[ + SizedBox(height: 16.h), + "Selected Appointment:".needTranslation.toText16(isBold: true), + SizedBox(height: 8.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + padding: EdgeInsets.all(16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (contactUsViewModel.patientFeedbackSelectedAppointment!.doctorNameObj!).toText16(isBold: true, maxlines: 1).toShimmer2(isShow: false), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: contactUsViewModel.patientFeedbackSelectedAppointment!.clinicName!).toShimmer2(isShow: false), + AppCustomChipWidget(labelText: contactUsViewModel.patientFeedbackSelectedAppointment!.projectName!).toShimmer2(isShow: false), + AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(contactUsViewModel.patientFeedbackSelectedAppointment!.appointmentDate), false), + ).toShimmer2(isShow: false), + ], + ), + ], + ), + ), + ], + ), + ), + ], + SizedBox(height: 16.h), + TextInputWidget( + labelText: "Subject".needTranslation, + hintText: "Enter subject here".needTranslation, + controller: subjectTextController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + SizedBox(height: 16.h), + TextInputWidget( + labelText: "Message".needTranslation, + hintText: "Enter message here".needTranslation, + controller: messageTextController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + isMultiline: true, + keyboardType: TextInputType.text, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + SizedBox(height: 16.h), + CustomButton( + text: LocaleKeys.selectAttachment.tr(context: context), + onPressed: () async { + ImageOptions.showImageOptionsNew( + context, + true, + (String image, file) { + print(image); + print(file); + Navigator.pop(context); + contactUsViewModel.addFeedbackAttachment(image); + }, + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppAssets.file_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ), + SizedBox(height: 16.h), + contactUsViewModel.feedbackAttachmentList.isNotEmpty + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: ListView.builder( + padding: EdgeInsets.all(16.h), + shrinkWrap: true, + itemCount: contactUsViewModel.feedbackAttachmentList.length, + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + ), + SizedBox(width: 8.w), + "Image ${index + 1}".toText14().paddingOnly(bottom: 8.h), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon).onPress(() { + contactUsViewModel.removeFeedbackAttachment(contactUsViewModel.feedbackAttachmentList[index]); + }), + ], + ); + }, + ), + ) + : SizedBox.shrink(), + ], + ); + } else { + return Container(); + } + } +} diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart new file mode 100644 index 0000000..5957bb8 --- /dev/null +++ b/lib/presentation/contact_us/find_us_page.dart @@ -0,0 +1,162 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/widgets/find_us_item_card.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/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:provider/provider.dart'; + +class FindUsPage extends StatelessWidget { + FindUsPage({super.key}); + + late AppState appState; + late ContactUsViewModel contactUsViewModel; + + @override + Widget build(BuildContext context) { + contactUsViewModel = Provider.of(context); + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.location.tr(), + child: Consumer(builder: (context, contactUsVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + contactUsVM.isHMGLocationsListLoading + ? SizedBox.shrink() + : CustomTabBar( + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, LocaleKeys.hmgHospitals.tr()), + CustomTabBarModel(null, LocaleKeys.pharmaciesList.tr()), + ], + onTabChange: (index) { + contactUsVM.setHMGHospitalsListSelected(index == 0); + }, + ).paddingSymmetrical(24.h, 0.h), + ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: contactUsVM.isHMGLocationsListLoading + ? 5 + : contactUsVM.isHMGHospitalsListSelected + ? contactUsVM.hmgHospitalsLocationsList.length + : contactUsVM.hmgPharmacyLocationsList.length, + itemBuilder: (context, index) { + return contactUsVM.isHMGLocationsListLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ).paddingSymmetrical(24.h, 0.h) + : contactUsVM.isHMGHospitalsListSelected + // ? contactUsVM.hmgHospitalsLocationsList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: FindUsItemCard( + getHMGLocationsModel: contactUsVM.hmgHospitalsLocationsList[index], + ), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: FindUsItemCard( + getHMGLocationsModel: contactUsVM.hmgPharmacyLocationsList[index], + ), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ); + // : Utils.getNoDataWidget( + // context, + // noDataText: "No any locations yet.".needTranslation, + // ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 24.h), + // FindUsItemCard(), + // FindUsItemCard(), + // FindUsItemCard(), + ], + ); + }), + ), + ); + } +} diff --git a/lib/presentation/contact_us/live_chat_page.dart b/lib/presentation/contact_us/live_chat_page.dart new file mode 100644 index 0000000..7cbdee3 --- /dev/null +++ b/lib/presentation/contact_us/live_chat_page.dart @@ -0,0 +1,176 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class LiveChatPage extends StatelessWidget { + LiveChatPage({super.key}); + + String chatURL = ""; + + late AppState appState; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, contactUsVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.liveChat.tr(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: contactUsVM.isLiveChatProjectsListLoading ? 5 : contactUsVM.liveChatProjectsList.length, + itemBuilder: (context, index) { + return contactUsVM.isLiveChatProjectsListLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ).paddingSymmetrical(24.h, 0.h) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: contactUsVM.selectedLiveChatProjectIndex == index ? AppColors.primaryRedColor : AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ("${appState.isArabic() ? contactUsVM.liveChatProjectsList[index].projectNameN! : contactUsVM.liveChatProjectsList[index].projectName!}\n${contactUsVM.liveChatProjectsList[index].distanceInKilometers!} KM") + .needTranslation + .toText14(isBold: true, color: contactUsVM.selectedLiveChatProjectIndex == index ? AppColors.whiteColor : AppColors.textColor), + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: contactUsVM.selectedLiveChatProjectIndex == index ? AppColors.whiteColor : AppColors.textColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).onPress(() { + contactUsVM.setSelectedLiveChatProjectIndex(index); + chatURL = + "https://chat.hmg.com/Index.aspx?Name=${appState.getAuthenticatedUser()!.firstName}&PatientID=${appState.getAuthenticatedUser()!.patientId}&MobileNo=${appState.getAuthenticatedUser()!.mobileNumber}&Language=${appState.isArabic() ? 'ar' : 'en'}&WorkGroup=${contactUsVM.liveChatProjectsList[index].value}"; + debugPrint("Chat URL: $chatURL"); + }), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 24.h), + ], + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: CustomButton( + text: LocaleKeys.liveChat.tr(context: context), + onPressed: () async { + Uri uri = Uri.parse(chatURL); + launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); + }, + backgroundColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor, + borderColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ); + }), + ); + } +} diff --git a/lib/presentation/contact_us/widgets/feedback_appointment_selection.dart b/lib/presentation/contact_us/widgets/feedback_appointment_selection.dart new file mode 100644 index 0000000..ca04069 --- /dev/null +++ b/lib/presentation/contact_us/widgets/feedback_appointment_selection.dart @@ -0,0 +1,70 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; + +class FeedbackAppointmentSelection extends StatelessWidget { + FeedbackAppointmentSelection({super.key}); + + late MedicalFileViewModel medicalFileViewModel; + late ContactUsViewModel contactUsViewModel; + + @override + Widget build(BuildContext context) { + medicalFileViewModel = Provider.of(context, listen: false); + contactUsViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: LocaleKeys.feedback.tr(), + isClose: true, + child: Column( + children: [ + ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: medicalFileViewModel.patientMedicalReportAppointmentHistoryList.length, + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: medicalFileViewModel.patientMedicalReportAppointmentHistoryList[index], + myAppointmentsViewModel: Provider.of(context, listen: false), + bookAppointmentsViewModel: Provider.of(context, listen: false), + medicalFileViewModel: medicalFileViewModel, + contactUsViewModel: contactUsViewModel, + isLoading: false, + isFromHomePage: false, + isFromMedicalReport: true, + isForFeedback: true, + ), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 24.h), + ], + ), + ); + } +} diff --git a/lib/presentation/contact_us/widgets/find_us_item_card.dart b/lib/presentation/contact_us/widgets/find_us_item_card.dart new file mode 100644 index 0000000..96375c0 --- /dev/null +++ b/lib/presentation/contact_us/widgets/find_us_item_card.dart @@ -0,0 +1,104 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_hmg_locations.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class FindUsItemCard extends StatelessWidget { + FindUsItemCard({super.key, required this.getHMGLocationsModel}); + + late AppState appState; + GetHMGLocationsModel getHMGLocationsModel; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [hospitalName, distanceInfo], + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ); + } + + Widget get hospitalName => Row( + children: [ + Image.network( + getHMGLocationsModel.projectImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 40.h, + height: 40.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false).paddingOnly(right: 10), + Expanded( + child: Text( + getHMGLocationsModel.locationName!, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: AppColors.blackColor, + ), + ), + ) + ], + ); + + Widget get distanceInfo => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: "${getHMGLocationsModel.distanceInKilometers ?? ""} km".needTranslation, + icon: AppAssets.location_red, + iconColor: AppColors.primaryRedColor, + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + Row( + children: [ + AppCustomChipWidget( + labelText: "Get Directions".needTranslation, + icon: AppAssets.directions_icon, + iconColor: AppColors.whiteColor, + backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + textColor: AppColors.whiteColor, + onChipTap: () { + MapsLauncher.launchCoordinates(double.parse(getHMGLocationsModel.latitude ?? "0.0"), double.parse(getHMGLocationsModel.longitude ?? "0.0"), getHMGLocationsModel.locationName!); + }, + ), + SizedBox(width: 4.w), + AppCustomChipWidget( + labelText: LocaleKeys.callNow.tr(), + icon: AppAssets.call_fill, + iconColor: AppColors.whiteColor, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 1.0), + textColor: AppColors.whiteColor, + onChipTap: () { + launchUrl(Uri.parse("tel://" + "${getHMGLocationsModel.phoneNumber}")); + }, + ), + ], + ), + ], + ); +} diff --git a/lib/presentation/covid19test/covid19_landing_page.dart b/lib/presentation/covid19test/covid19_landing_page.dart new file mode 100644 index 0000000..62bd651 --- /dev/null +++ b/lib/presentation/covid19test/covid19_landing_page.dart @@ -0,0 +1,283 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +import 'covid_19_questionnaire.dart'; + +class Covid19LandingPage extends StatefulWidget { +const Covid19LandingPage({super.key}); + +@override +State createState() => _Covid19LandingPageState(); +} + +class _Covid19LandingPageState extends State { + + late HabibWalletViewModel habibWalletVM; + int? _selectedBranchIndex; + + @override + void initState() { + habibWalletVM = Provider.of(context, listen: false); + scheduleMicrotask(() { + getProjectList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "COVID-19", + child: Padding( + padding: EdgeInsets.all(24.w), + child: SingleChildScrollView( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Get the results in Few Hours" + .toText18( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + SizedBox(height: 16.h), + LocaleKeys.covid_info + .tr() + .toText14( + color: AppColors.greyTextColor, + weight: FontWeight.w400, + height: 1.6, + ), + ], + ), + ), + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + // borderRadius: 24.r, + hasShadow: true, + customBorder: BorderRadius.only(topLeft:Radius.circular(24.r) , topRight:Radius.circular(24.r)) + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomButton( + text: "SelectLocation".needTranslation, + onPressed: () { + _showBranchBottomSheet(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 50.h, + iconSize: 18.h, + ).paddingSymmetrical(16.h, 24.w), + ], + ), + ), + ), + ], + )); + } + void _showBranchBottomSheet(BuildContext context, ) { + // Set first branch as selected by default + setState(() { + _selectedBranchIndex = 0; + }); + + showCommonBottomSheet( + + context, + title: "Select Branch".needTranslation, + height: ResponsiveExtension.screenHeight * 0.651, + child: StatefulBuilder( + builder: (context, setBottomSheetState) { + return Consumer( + builder: (context, habibWalletVM, child) { + + final hospitals = habibWalletVM.advancePaymentHospitals; + if (hospitals.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No branches available'), + ), + ); + } + + return Column( + children: [ + Expanded( + child:Container( + margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h, bottom: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ), child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemBuilder: (context, index) { + final branch = hospitals[index]; + final isSelected = _selectedBranchIndex == index; + return GestureDetector( + onTap: () { + setBottomSheetState(() { + _selectedBranchIndex = index; + }); + setState(() { + _selectedBranchIndex = index; + }); + }, + child: Container( + + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Row( + children: [ + // Radio button + Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 10.w, + height: 10.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryRedColor, + ), + ), + ) + : null, + ), + SizedBox(width: 12.w), + // Branch details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + SizedBox(height: 4.h), + (branch.desciption ?? 'Unknown').toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ], + ), + ), + // Location icon + + ], + ), + ), + ); + }, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemCount: hospitals.length, + )), + ), + // Next button + Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ), + child: SafeArea( + top: false, + child: CustomButton( + text: "Next".needTranslation, + onPressed: (){ + + Navigator.of(context) + .push( + CustomPageRoute( + page: Covid19Questionnaire(selectedHospital: hospitals[_selectedBranchIndex!],), + ), + ); + + }, + backgroundColor: _selectedBranchIndex != null + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + borderColor: _selectedBranchIndex != null + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 56.h, + ), + ), + ), + ], + ); + }, + ); + }, + ), + + ); + } + + getProjectList() async{ + await habibWalletVM.getProjectsList(); + } +} + + diff --git a/lib/presentation/covid19test/covid_19_questionnaire.dart b/lib/presentation/covid19test/covid_19_questionnaire.dart new file mode 100644 index 0000000..8608d80 --- /dev/null +++ b/lib/presentation/covid19test/covid_19_questionnaire.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:flutter/material.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/covid_questionnare_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid_review_screen.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/CustomSwitch.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +import 'package:provider/provider.dart'; + +class Covid19Questionnaire extends StatefulWidget { + final HospitalsModel selectedHospital; + const Covid19Questionnaire({super.key, required this.selectedHospital}); + + @override + State createState() => _Covid19QuestionnaireState(); +} + +class _Covid19QuestionnaireState extends State { + late HmgServicesViewModel hmgServicesViewModel; + List qaList = []; + + + @override + void initState() { + hmgServicesViewModel = Provider.of(context, listen: false); + scheduleMicrotask(() { + setState(() { + qaList = hmgServicesViewModel.getQuestionsFromJson(); + }); + }); + super.initState(); + } + + void _toggleAnswer(int index, bool value) { + setState(() { + qaList[index].ans = value ? 1 : 0; + }); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "COVID-19", + bottomChild: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ),child: CustomButton( + text: "Next".needTranslation, + onPressed: () { + moveToNextPage(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 56.h, + )), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Please answer below questionnaire:".toText14( + color: AppColors.textColor, + weight: FontWeight.w500, + ), + SizedBox(height: 20.h), + // Question list + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: qaList.length, + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemBuilder: (context, index) { + final question = qaList[index]; + final isAnswerYes = question.ans == 1; + + return Row( + children: [ + Expanded( + child: (question.questionEn ?? '').toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + ), + SizedBox(width: 12.w), + CustomSwitch( + value: isAnswerYes, + onChanged: (value) => _toggleAnswer(index, value), + ), + ], + ); + }, + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + // Next button + + ], + ), + ), + + ), + ); + + + } + moveToNextPage(BuildContext context) async{ + LoaderBottomSheet.showLoader(); + await hmgServicesViewModel.getCovidProcedureList(); + await hmgServicesViewModel.getPaymentInfo(procedureID: hmgServicesViewModel.covidTestProcedureList[0].procedureId!); + LoaderBottomSheet.hideLoader(); + Navigator.of(context) + .push( + CustomPageRoute( + page: CovidReviewScreen(selectedHospital: widget.selectedHospital, qaList: qaList), + ), + ); + } +} diff --git a/lib/presentation/covid19test/covid_payment_screen.dart b/lib/presentation/covid19test/covid_payment_screen.dart new file mode 100644 index 0000000..42e7736 --- /dev/null +++ b/lib/presentation/covid19test/covid_payment_screen.dart @@ -0,0 +1,565 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +// Added imports required by this file +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; + +/// A reusable payment screen for COVID-related payments. +/// +/// This screen re-uses the same UI pattern and payment flow used by +/// `AppointmentPaymentPage` (in-app browser, Apple Pay and payfort status checks), +/// but it keeps the post-payment handling generic (shows success / failure) +/// so it can be safely used for COVID test purchases without appointment-specific APIs. +class CovidPaymentScreen extends StatefulWidget { + final double amount; + final int projectID; + final int clinicID; + final String procedureId; + final double taxAmount; + final String title; + + const CovidPaymentScreen({ + super.key, + required this.amount, + required this.projectID, + required this.clinicID, + required this.procedureId, + this.taxAmount = 0.0, + this.title = "COVID Payment", + }); + + @override + State createState() => _CovidPaymentScreenState(); +} + +class _CovidPaymentScreenState extends State { + late PayfortViewModel payfortViewModel; + late AppState appState; + + MyInAppBrowser? browser; + String selectedPaymentMethod = ""; + String transID = ""; + + bool isShowTamara = false; // placeholder: could be enabled based on remote config + + @override + void initState() { + super.initState(); + // initialize payfort view model when the widget is ready + scheduleMicrotask(() { + payfortViewModel = Provider.of(context, listen: false); + payfortViewModel.initPayfortViewModel(); + payfortViewModel.setIsApplePayConfigurationLoading(false); + // Optionally compute if Tamara should be shown by calling a remote config API. + // For now keep it false (unless the app provides an API for it). + // Enable Tamara payment option for COVID screen + setState(() { + isShowTamara = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + payfortViewModel = Provider.of(context); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: widget.title.needTranslation, + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(( (widget.amount - widget.taxAmount).toString()).toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + // Show VAT amount passed from review screen + Utils.getPaymentAmountWithSymbol((widget.taxAmount.toString()).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.amount.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + + // Apple Pay (iOS) + Platform.isIOS + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (Utils.havePrivilege(103)) { + startApplePay(); + } else { + openPaymentURL("ApplePay"); + } + }) + : SizedBox(height: 12.h), + SizedBox(height: 12.h), + + // Action buttons: Cancel + Next (Next opens default payment flow - e.g. Visa) + // Padding( + // padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h), + // child: Row( + // children: [ + // Expanded( + // child: CustomButton( + // height: 56.h, + // text: LocaleKeys.cancel.tr(), + // onPressed: () { + // Navigator.of(context).pop(); + // }, + // backgroundColor: AppColors.secondaryLightRedColor, + // borderColor: AppColors.secondaryLightRedColor, + // textColor: AppColors.primaryRedColor, + // icon: AppAssets.cancel, + // iconColor: AppColors.primaryRedColor, + // borderRadius: 12.r, + // ), + // ), + // SizedBox(width: 8.h), + // Expanded( + // child: CustomButton( + // height: 56.h, + // text: "Next".needTranslation, + // onPressed: () { + // // Default to Visa for Next + // selectedPaymentMethod = "VISA"; + // openPaymentURL("visa"); + // }, + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // fontSize: 16.f, + // fontWeight: FontWeight.w500, + // borderRadius: 12.r, + // padding: EdgeInsets.symmetric(horizontal: 10.w), + // icon: AppAssets.add_icon, + // iconColor: AppColors.whiteColor, + // iconSize: 18.h, + // ), + // ), + // ], + // ), + // ), + ], + ), + ).paddingSymmetrical(0.h, 0.h), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + + // MADA tile + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "MADA"; + openPaymentURL("MADA"); + }), + + SizedBox(height: 16.h), + + // Visa / Mastercard tile + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), + ], + ), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "VISA"; + openPaymentURL("VISA"); + }), + + SizedBox(height: 16.h), + + // Optional Tamara tile (shown only if enabled) + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + AppAssets.tamaraEng, + width: 72.h, + height: 25.h, + fit: BoxFit.contain, + // If PNG fails to load for any reason, log and fallback to SVG asset + errorBuilder: (context, error, stackTrace) { + debugPrint('Failed to load Tamara PNG asset: $error'); + return Utils.buildSvgWithAssets( + icon: AppAssets.tamara, + width: 72.h, + height: 25.h, + fit: BoxFit.contain, + ); + }, + ), + SizedBox(height: 16.h), + "Tamara".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("TAMARA"); + }) + : SizedBox.shrink(), + + SizedBox(height: 24.h), + + // Bottom payment summary + + SizedBox(height: 24.h), + ], + ), + ), + + ), + ); + } + + void onBrowserLoadStart(String url) { + // Generic loader hook: detect success / error patterns from the in-app browser. + // Keep parsing defensive: use Uri.parse where possible. + try { + final uri = Uri.tryParse(url); + if (selectedPaymentMethod == "TAMARA" && uri != null) { + // tamara returns different query param names depending on platform; defensive checks + final params = uri.queryParameters; + if (params.isNotEmpty) { + // example keys: 'status', 'AuthorizePaymentId' (android) or 'paymentStatus', 'orderId' (iOS) + } + } + } catch (e) { + debugPrint('onBrowserLoadStart parse error: $e'); + } + + MyInAppBrowser.successURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + }); + + MyInAppBrowser.errorURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + }); + } + + void onBrowserExit(bool isPaymentMade) async { + // When browser closes, check payment status using payfort view model + await checkPaymentStatus(); + } + + Future checkPaymentStatus() async { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + try { + await payfortViewModel.checkPaymentStatus(transactionID: transID, onSuccess: (apiResponse) async { + // treat any successful responseMessage as success; otherwise show generic error + final success = payfortViewModel.payfortCheckPaymentStatusResponseModel?.responseMessage?.toLowerCase() == 'success'; + LoaderBottomSheet.hideLoader(); + if (success) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Payment successful".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } catch (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + + void openPaymentURL(String paymentMethod) { + browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context); + transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch); + + // Open payment browser with essential parameters; many fields are simplified here + browser?.openPaymentBrowser( + widget.amount, + "COVID Test Payment", + transID, + widget.projectID.toString(), + "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + selectedPaymentMethod, + appState.getAuthenticatedUser()?.patientType.toString() ?? "", + appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + appState.getAuthenticatedUser()?.patientId.toString() ?? "", + appState.getAuthenticatedUser() ?? (null as dynamic), + browser!, + false, + "2", + "", + context, + DateTime.now().toString(), + "", + 0, + 0, + "3", + ); + } + + void startApplePay() async { + showCommonBottomSheet( + context, + child: Utils.getLoadingWidget(), + callBackFunc: (str) {}, + title: "", + height: ResponsiveExtension.screenHeight * 0.3, + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + + transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch); + + // Prepare a minimal apple pay request using payfortViewModel's configuration + try { + await payfortViewModel.getPayfortConfigurations(serviceId: 0, projectId: widget.projectID, integrationId: 2); + + // Build minimal apple pay request (model omitted here to keep things generic) + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest( + clientRequestID: transID, + clinicID: widget.clinicID, + currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED", + customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + customerID: appState.getAuthenticatedUser()?.patientId, + customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + deviceToken: await Utils.getStringFromPrefs(CacheConst.pushToken), + voipToken: await Utils.getStringFromPrefs(CacheConst.voipToken), + doctorID: 0, + projectID: widget.projectID.toString(), + serviceID: "0", + channelID: 3, + patientID: appState.getAuthenticatedUser()?.patientId, + patientTypeID: appState.getAuthenticatedUser()?.patientType, + patientOutSA: appState.getAuthenticatedUser()?.outSa, + appointmentDate: DateTime.now().toString(), + appointmentNo: 0, + orderDescription: "COVID Test Payment", + liveServiceID: "0", + latitude: "0.0", + longitude: "0.0", + amount: widget.amount.toString(), + isSchedule: "0", + language: appState.isArabic() ? 'ar' : 'en', + languageID: appState.isArabic() ? 1 : 2, + userName: appState.getAuthenticatedUser()?.patientId, + responseContinueURL: "http://hmg.com/Documents/success.html", + backClickUrl: "http://hmg.com/Documents/success.html", + paymentOption: "ApplePay", + isMobSDK: true, + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier, + commandType: "PURCHASE", + signature: payfortViewModel.payfortProjectDetailsRespModel?.signature, + accessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode, + shaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest, + shaResponsePhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaResponse, + returnURL: "", + ); + + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) { + // Start apple pay flow + payfortViewModel.paymentWithApplePay( + customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + orderDescription: "COVID Test Payment", + orderAmount: widget.amount, + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier ?? "", + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode ?? "", + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest ?? "", + currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + Navigator.of(context).pop(); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + await checkPaymentStatus(); + }, + ); + }).catchError((e) { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: e.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } catch (e) { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: e.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + } + diff --git a/lib/presentation/covid19test/covid_review_screen.dart b/lib/presentation/covid19test/covid_review_screen.dart new file mode 100644 index 0000000..7ccbb02 --- /dev/null +++ b/lib/presentation/covid19test/covid_review_screen.dart @@ -0,0 +1,441 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/covid_questionnare_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid_payment_screen.dart'; + +class CovidReviewScreen extends StatefulWidget { + + final HospitalsModel selectedHospital; + final List qaList; + + const CovidReviewScreen({super.key, required this.selectedHospital, required this.qaList}); + + @override + State createState() => _CovidReviewScreenState(); +} + +class _CovidReviewScreenState extends State { + + late HmgServicesViewModel hmgServicesViewModel; + bool _acceptedTerms =false; + Covid19GetTestProceduresResp? _selectedProcedure; + @override + void initState() { + super.initState(); + hmgServicesViewModel = Provider.of(context, listen: false); + scheduleMicrotask(() { + + }); + + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "COVID-19", + bottomChild: Consumer(builder: (context, vm, _) { + final info = vm.covidPaymentInfo; + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info == null) ...[ + // show a placeholder/loading while payment info is null + SizedBox(height: 24.h), + Center(child: CircularProgressIndicator()), + SizedBox(height: 24.h), + ] else ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText18(isBold: true), + Utils.getPaymentAmountWithSymbol( + (info.patientShareField ?? 0).toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true), + ], + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Tax Amount".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + (info.patientTaxAmountField ?? 0).toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true) + ], + ), + SizedBox(height: 18.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150.h, + child: Utils.getPaymentMethods(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Utils.getPaymentAmountWithSymbol( + (info.patientShareWithTaxField ?? 0).toString().toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true), + ], + ), + ], + ) + ], + ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), + + GestureDetector( + onTap: () { + setState(() { + _acceptedTerms = !_acceptedTerms; + }); + }, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + child: Row( + children: [ + Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + color: _acceptedTerms + ? AppColors.primaryRedColor + : AppColors.whiteColor, + border: Border.all( + color: _acceptedTerms + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + borderRadius: BorderRadius.circular(4.r), + ), + child: _acceptedTerms + ? Icon( + Icons.check, + size: 14.h, + color: AppColors.whiteColor, + ) + : null, + ), + SizedBox(width: 12.w), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 14.f, + color: AppColors.textColor, + fontWeight: FontWeight.w400, + ), + children: [ + const TextSpan(text: "I agree to the "), + TextSpan( + text: "terms and conditions", + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + decorationColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Two-button layout: Cancel (left) and Next (right) + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h), + child: Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.cancel.tr(), + onPressed: () { + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + // icon: AppAssets.cancel, + // iconColor: AppColors.primaryRedColor, + borderRadius: 12.r, + ), + ), + SizedBox(width: 8.h), + Expanded( + child: CustomButton( + height: 56.h, + text: "Next".needTranslation, + onPressed: () async { + // Validate selection and payment info + if (_selectedProcedure == null) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Please select a procedure".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + return; + } + + if (info == null) { + // If payment info missing, attempt to fetch + LoaderBottomSheet.showLoader(); + try { + await hmgServicesViewModel.getPaymentInfo(procedureID: _selectedProcedure!.procedureId!); + } catch (e) { + debugPrint('getPaymentInfo error: $e'); + } finally { + LoaderBottomSheet.hideLoader(); + } + + if (hmgServicesViewModel.covidPaymentInfo == null) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment information not available".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + return; + } + } + + // Compute amount and project/clinic IDs defensively + final paymentInfo = hmgServicesViewModel.covidPaymentInfo ?? info!; + final double amount = paymentInfo.patientShareWithTaxField ?? (paymentInfo.patientShareField?.toDouble() ?? 0.0); + + // projectID may be int or string; parse defensively + int projectID = 0; + try { + final p = widget.selectedHospital.mainProjectID; + if (p is int) projectID = p; + else if (p is String) projectID = int.tryParse(p) ?? 0; + } catch (_) {} + + int clinicID = 0; + try { + clinicID = int.tryParse(widget.selectedHospital.setupID ?? '') ?? int.tryParse(widget.selectedHospital.iD?.toString() ?? '') ?? 0; + } catch (_) {} + + // Navigate to payment screen + Navigator.of(context).push( + CustomPageRoute( + page: CovidPaymentScreen( + amount: amount, + projectID: projectID, + clinicID: clinicID, + procedureId: _selectedProcedure!.procedureId ?? '', + taxAmount: paymentInfo.patientTaxAmountField?.toDouble() ?? 0.0, + ), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + // icon: AppAssets.add_icon, + // iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ), + ], + ), + ) + ] + ], + ), + ), + ); + }), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Please select the procedure:".toText14( + color: AppColors.textColor, + weight: FontWeight.w500, + ), + SizedBox(height: 16.h), + + Consumer( + builder: (context, vm, _) { + final procedures = vm.covidTestProcedureList; + + // ensure default selection is first item (once available) + if (_selectedProcedure == null && procedures.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _selectedProcedure = procedures[0]; + }); + + // also fetch payment info for default selection and show loader while loading + if (procedures[0].procedureId != null) { + LoaderBottomSheet.showLoader(); + hmgServicesViewModel + .getPaymentInfo(procedureID: procedures[0].procedureId!) + .whenComplete(() { + LoaderBottomSheet.hideLoader(); + }); + } + } + }); + } + + // Use a shrink-wrapped ListView.separated and match prescription styling + return Container( + + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: procedures.length, + separatorBuilder: (context, index) => Divider( + height: 1.h, + thickness: 1.h, + color: AppColors.borderOnlyColor.withValues(alpha: 0.05), + ).paddingOnly(top:8,bottom:16.h), + itemBuilder: (context, index) { + final item = procedures[index]; + // Let the radio option widget manage its own padding to avoid doubled spacing + return _buildRadioOption(value: item, title: item.procedureName ?? ''); + }, + ).paddingOnly(bottom:16.h) + ), + ); + }, + ), + ], + ), + ), + ), + ); + + } + + Widget _buildRadioOption({ + required Covid19GetTestProceduresResp value, + required String title, + }) { + final bool isSelected = _selectedProcedure?.procedureId == value.procedureId; + + return GestureDetector( + onTap: () async { + setState(() { + _selectedProcedure = value; + }); + + // show bottomsheet loader while fetching payment info + if (value.procedureId != null) { + LoaderBottomSheet.showLoader(); + try { + await hmgServicesViewModel.getPaymentInfo(procedureID: value.procedureId!, projectID: widget.selectedHospital.mainProjectID); + } catch (e) { + debugPrint('getPaymentInfo error: $e'); + } finally { + LoaderBottomSheet.hideLoader(); + } + } + }, + + + child: Row( + children: [ + Container( + width: 20.h, + height: 20.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 10.h, + height: 10.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryRedColor, + ), + ), + ) + : null, + ), + SizedBox(width: 12.h), + Expanded( + child: title.toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + ), + ], + ) + // Keep only bottom padding here and rely on the surrounding container's left/right inset + .paddingOnly(left: 0.h, right: 0.h, top: 0.h, bottom: 12.h), + ); + + } + } diff --git a/lib/presentation/e_referral/e-referral_validator.dart b/lib/presentation/e_referral/e-referral_validator.dart new file mode 100644 index 0000000..e0a8006 --- /dev/null +++ b/lib/presentation/e_referral/e-referral_validator.dart @@ -0,0 +1,85 @@ +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/e_referral_form_model.dart'; + +class ReferralValidator { + static FormValidationErrors validateStep1(ReferralFormData formData) { + final errors = FormValidationErrors(); + + if (formData.requesterName.trim().isEmpty) { + errors.requesterName = 'Referral requester name is required'.needTranslation; + } + + + + if (formData.relationship == null) { + errors.relationship = 'Please select a relationship'.needTranslation; + } + + if (formData.relationship != null && + formData.relationship?.iD == 5 && + formData.otherRelationshipName.trim().isEmpty) { + errors.otherRelationshipName = 'Other relationship name is required'.needTranslation; + } + + return errors; + } + + static FormValidationErrors validateStep2(ReferralFormData formData) { + final errors = FormValidationErrors(); + + if (formData.patientIdentification.trim().isEmpty) { + errors.patientIdentification = 'Identification number is required'.needTranslation; + } + + if (formData.patientName.trim().isEmpty) { + errors.patientName = 'Patient name is required'.needTranslation; + } + + if (formData.patientPhone == null) { + errors.patientPhone = 'Please Enter patient phone number'.needTranslation; + } + + if (formData.patientCity == null) { + errors.patientCity = 'Please select patient city'.needTranslation; + } + + return errors; + } + + static FormValidationErrors validateStep3(ReferralFormData formData) { + final errors = FormValidationErrors(); + + if (formData.medicalReportImages.isEmpty) { + errors.medicalReport = 'At least one medical report is required'.needTranslation; + } + + if (formData.branch == null) { + errors.branch = 'Please select a branch'.needTranslation; + } + + if (formData.isPatientInsured && formData.insuredPatientImages.isEmpty) { + errors.insuredDocument = 'Insurance document is required for insured patients'.needTranslation; + } + + return errors; + } + + static bool _isValidSaudiPhone(String phone) { + final regex = RegExp(r'^5\d{8}$'); + return regex.hasMatch(phone); + } + + static bool hasErrors(FormValidationErrors errors) { + return errors.requesterName != null || + errors.requesterPhone != null || + errors.relationship != null || + errors.otherRelationshipName != null || + errors.patientIdentification != null || + errors.patientName != null || + errors.patientPhone != null || + errors.patientCity != null || + errors.medicalReport != null || + errors.branch != null || + errors.insuredDocument != null; + } +} \ No newline at end of file diff --git a/lib/presentation/e_referral/e_referral_form_manager.dart b/lib/presentation/e_referral/e_referral_form_manager.dart new file mode 100644 index 0000000..9e5cbe8 --- /dev/null +++ b/lib/presentation/e_referral/e_referral_form_manager.dart @@ -0,0 +1,289 @@ +// managers/referral_form_manager.dart +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/relationship_type_resp_mode.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/e_referral_form_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; + + +class ReferralFormManager extends ChangeNotifier { + final ReferralFormData _formData = ReferralFormData(); + final FormValidationErrors _errors = FormValidationErrors(); + + ReferralFormData get formData => _formData; + FormValidationErrors get errors => _errors; + + + int _searchCriteria = 0; // 0 = Identification Number, 1 = Referral Number + String? _searchValue; + String? _searchPhone; + +// Getters + int get searchCriteria => _searchCriteria; + String? get searchValue => _searchValue; + String? get searchPhone => _searchPhone; + + void updateRequesterName(String value) { + _formData.requesterName = value; + _clearError('requesterName'); + } + + void updateRequesterPhone(String value) { + _formData.requesterPhone = value; + _clearError('requesterPhone'); + } + + void updateCountryEnum(CountryEnum value) { + _formData.countryEnum = value; + } + void updatePatientCountryEnum(CountryEnum value) { + _formData.patientCountryEnum = value; + } + + void updateRelationship(GetAllRelationshipTypeResponseModel? value) { + _formData.relationship = value; + notifyListeners(); + _clearError('relationship'); + } + + void updateOtherRelationshipName(String value) { + _formData.otherRelationshipName = value; + _clearError('otherRelationshipName'); + } + + void updatePatientIdentification(String value) { + _formData.patientIdentification = value; + _clearError('patientIdentification'); + } + + void updatePatientName(String value) { + _formData.patientName = value; + _clearError('patientName'); + } + + void updatePatientPhone(String? value) { + _formData.patientPhone = value!; + _clearError('patientPhone'); + } + + void updatePatientCity(GetAllCitiesResponseModel? value) { + _formData.patientCity = value; + _clearError('patientCity'); + notifyListeners(); + } + + void updateBranch(HospitalsModel? value) { + _formData.branch = value; + _clearError('branch'); + } + + void updateIsPatientInsured(bool value) { + _formData.isPatientInsured = value; + if (!value) { + _formData.insuredPatientImages.clear(); + } + _clearError('insuredDocument'); + } + + void addMedicalReport(EReferralAttachment attachment) { + _formData.medicalReportImages.add(attachment); + _clearError('medicalReport'); + } + + void removeMedicalReport(int index) { + if (index >= 0 && index < _formData.medicalReportImages.length) { + _formData.medicalReportImages.removeAt(index); + } + notifyListeners(); + } + + void addInsuranceDocument(EReferralAttachment attachment) { + _formData.insuredPatientImages.add(attachment); + _clearError('insuredDocument'); + } + + void removeInsuranceDocument(int index) { + if (index >= 0 && index < _formData.insuredPatientImages.length) { + _formData.insuredPatientImages.removeAt(index); + } + + notifyListeners(); + } + + + // Error management + void setErrors(FormValidationErrors newErrors) { + _errors.requesterName = newErrors.requesterName; + _errors.requesterPhone = newErrors.requesterPhone; + _errors.relationship = newErrors.relationship; + _errors.otherRelationshipName = newErrors.otherRelationshipName; + _errors.patientIdentification = newErrors.patientIdentification; + _errors.patientName = newErrors.patientName; + _errors.patientPhone = newErrors.patientPhone; + _errors.patientCity = newErrors.patientCity; + _errors.medicalReport = newErrors.medicalReport; + _errors.branch = newErrors.branch; + _errors.insuredDocument = newErrors.insuredDocument; + notifyListeners(); // Only notify when errors change + } + + void clearAllErrors() { + _errors.requesterName = null; + _errors.requesterPhone = null; + _errors.relationship = null; + _errors.otherRelationshipName = null; + _errors.patientIdentification = null; + _errors.patientName = null; + _errors.patientPhone = null; + _errors.patientCity = null; + _errors.medicalReport = null; + _errors.branch = null; + _errors.insuredDocument = null; + notifyListeners(); + } + + void _clearError(String field) { + bool shouldNotify = false; + + switch (field) { + case 'requesterName': + if (_errors.requesterName != null) { + _errors.requesterName = null; + shouldNotify = true; + } + break; + case 'requesterPhone': + if (_errors.requesterPhone != null) { + _errors.requesterPhone = null; + shouldNotify = true; + } + break; + case 'relationship': + if (_errors.relationship != null) { + _errors.relationship = null; + shouldNotify = true; + } + break; + case 'otherRelationshipName': + if (_errors.otherRelationshipName != null) { + _errors.otherRelationshipName = null; + shouldNotify = true; + } + break; + case 'patientIdentification': + if (_errors.patientIdentification != null) { + _errors.patientIdentification = null; + shouldNotify = true; + } + break; + case 'patientName': + if (_errors.patientName != null) { + _errors.patientName = null; + shouldNotify = true; + } + break; + case 'patientPhone': + if (_errors.patientPhone != null) { + _errors.patientPhone = null; + shouldNotify = true; + } + break; + case 'patientCity': + if (_errors.patientCity != null) { + _errors.patientCity = null; + shouldNotify = true; + } + break; + case 'medicalReport': + if (_errors.medicalReport != null) { + _errors.medicalReport = null; + shouldNotify = true; + } + break; + case 'branch': + if (_errors.branch != null) { + _errors.branch = null; + shouldNotify = true; + } + break; + case 'insuredDocument': + if (_errors.insuredDocument != null) { + _errors.insuredDocument = null; + shouldNotify = true; + } + break; + } + + if (shouldNotify) { + notifyListeners(); + } + } + void updateSearchCriteria(int criteria) { + _searchCriteria = criteria; + _errors.searchValue = _validateSearchValue(); + notifyListeners(); + } + + void updateSearchValue(String value) { + _searchValue = value; + _errors.searchValue = _validateSearchValue(); + } + void updateSearchPhone(String value) { + _searchPhone = value; + _errors.searchPhone = _validateSearchPhone(); + } + + String? _validateSearchValue() { + if (_searchValue == null || _searchValue!.isEmpty) { + return _searchCriteria == 0 + ? 'Identification Number is required' + : 'Referral Number is required'; + } + + if (_searchCriteria == 0) { + // Validate identification number format + if (!_isValidIdentificationNumber(_searchValue!)) { + return 'Please enter a valid identification number'; + } + } else { + // Validate referral number format + if (!_isValidReferralNumber(_searchValue!)) { + return 'Please enter a valid referral number'; + } + } + + return null; + } + + bool _isValidIdentificationNumber(String value) { + // Add your identification number validation logic + return value.isNotEmpty; // Basic validation + } + + bool _isValidReferralNumber(String value) { + // Add your referral number validation logic + return value.isNotEmpty; // Basic validation + } + + void validateSearchForm() { + _errors.searchValue = _validateSearchValue(); + _errors.searchPhone = _validateSearchPhone(); + notifyListeners(); + } + + String? _validateSearchPhone() { + if (_searchPhone == null || _searchPhone!.isEmpty) { + return 'Phone number is required'; + } + return null; + } + bool get isSearchFormInValid { + return ( + _errors.searchValue != null && + _errors.searchValue!.isNotEmpty || + _errors.searchPhone != null && + _errors.searchPhone!.isNotEmpty); + } +} \ No newline at end of file diff --git a/lib/presentation/e_referral/e_referral_search_result.dart b/lib/presentation/e_referral/e_referral_search_result.dart new file mode 100644 index 0000000..8702d5a --- /dev/null +++ b/lib/presentation/e_referral/e_referral_search_result.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class SearchResultPage extends StatefulWidget { + const SearchResultPage({super.key}); + + @override + _SearchResultPageState createState() => _SearchResultPageState(); +} + +class _SearchResultPageState extends State { + HmgServicesViewModel? hmgServicesVM; + + String _selectedFilter = 'All'; + + @override + void initState() { + hmgServicesVM = context.read(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Search Result".needTranslation, + child: Column( + children: [ + // List of referrals + ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: hmgServicesVM?.searchReferralList.length, + itemBuilder: (context, index) { + return _buildReferralCard(hmgServicesVM!.searchReferralList[index]); + }, + ), + ], + ), + ); + } + + Widget _buildReferralCard(SearchEReferralResponseModel referral) { + return SmoothCard( + borderRadius: BorderRadius.circular(16.h), + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: AppColors.whiteColor, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + 'Referral No ${referral.referralNumber}'.needTranslation.toText18(isBold: true, color: AppColors.textColor), + + + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _getStatusColor(referral.status!), + borderRadius: BorderRadius.circular(20), + ), + child: + referral.status!.toText12(color: AppColors.whiteColor, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // ), + ), + ), + ], + ), + + SizedBox(height: 16), + + // Patient information + Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: AppColors.lightGrayBGColor, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person, + color: AppColors.lightGrayBGColor, + size: 30, + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //Text( + referral.patientName!.toText16(isBold: true, color: AppColors.textColor), + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // ), + // ), + SizedBox(height: 4), + // Text( + 'ID: ${referral.identityNumber}'.toText14(color: AppColors.greyTextColor), + // style: TextStyle( + // color: Colors.grey[600], + // ), + // ), + ], + ), + ), + ], + ), + + SizedBox(height: 16), + + // Details row + Row( + children: [ + _buildDetailItem( + Icons.phone, + referral.patientContactNo!, + ), + SizedBox(width: 16), + _buildDetailItem(Icons.calendar_today, Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDateNoTimeZone(referral.referralDate!))), + ], + ), + + SizedBox(height: 16), + + // Requester information + Container( + padding: EdgeInsets.all(5.h), + decoration: BoxDecoration( + color: AppColors.lightGrayBGColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.person_outline, + color: Colors.grey[600], + size: 20, + ), + SizedBox(width: 8), + //Text( + 'Requester: ${referral.requesterName}'.toText14(), + // style: TextStyle( + // color: Colors.grey[700], + // ), + // ), + SizedBox(width: 16), + Icon( + Icons.phone, + color: Colors.grey[600], + size: 16, + ), + SizedBox(width: 4), + // Text( + referral.requesterContactNo!.toText14(), + // style: TextStyle( + // color: Colors.grey[700], + // ), + // ), + ], + ), + ), + + SizedBox(height: 12), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.group, + color: Colors.grey[600], + size: 16, + ), + SizedBox(width: 4), + //Text( + 'Relationship: ${referral.relationshipType?.text}'.toText14(color:AppColors.greyTextColor), + // style: TextStyle( + // color: Colors.grey[700], + // fontSize: 14, + // ), + // ), + ], + ), + Row( + children: [ + Icon( + Icons.attach_file, + color: Colors.grey[600], + size: 16, + ), + SizedBox(width: 4), + // Text( + '${referral.medicalReportAttachment?.length} file(s)'.toText14(color:AppColors.greyTextColor), + // style: TextStyle( + // color: Colors.grey[700], + // fontSize: 14, + // ), + // ), + ], + ), + ], + ), + + SizedBox(height: 16), + + // Action buttons + ], + ), + ), + ); + } + + Widget _buildDetailItem(IconData icon, String text) { + return Row( + children: [ + Icon( + icon, + color: Colors.grey[600], + size: 16, + ), + SizedBox(width: 4), + + text.toText14(color: AppColors.greyTextColor), + + ], + ); + } + + Color _getStatusColor(String status) { + switch (status) { + case 'Pending': + return AppColors.alertColor; + case 'Completed': + return AppColors.bgGreenColor; + case 'Rejected': + return AppColors.primaryRedColor; + default: + return AppColors.lightGrayColor; + } + } + + + Widget _buildFilterOption(String filter) { + return ListTile( + leading: Icon( + _selectedFilter == filter ? Icons.radio_button_checked : Icons.radio_button_off, + color: _selectedFilter == filter ? Colors.blue[700] : Colors.grey, + ), + title: Text(filter), + onTap: () { + setState(() { + _selectedFilter = filter; + }); + Navigator.pop(context); + }, + ); + } +} diff --git a/lib/presentation/e_referral/new_e_referral.dart b/lib/presentation/e_referral/new_e_referral.dart new file mode 100644 index 0000000..3083de1 --- /dev/null +++ b/lib/presentation/e_referral/new_e_referral.dart @@ -0,0 +1,261 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/e_referral_form_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/search_e_referral.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/widget/e-referral_otp.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/widget/e_referral_other_details.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/widget/e_referral_patient_info.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/widget/e_referral_requester_form.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; +import 'package:provider/provider.dart'; + +import 'e-referral_validator.dart'; +import 'e_referral_form_manager.dart'; + +class NewReferralPage extends StatefulWidget { + const NewReferralPage({ + super.key, + }); + + @override + State createState() => _NewReferralPageState(); +} + +class _NewReferralPageState extends State { + final PageController _pageController = PageController(); + final ReferralFormManager _formManager = ReferralFormManager(); // Use manager + int _currentStep = 0; + + double widthOfOneState = ((ResponsiveExtension.screenWidth) / 3) - (20.h); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadData(); + }); + } + + void _handleNextStep() { + switch (_currentStep) { + case 0: + if (_validateCurrentStep()) { + OTPService.openOTPScreen( + context: context, + formManager: _formManager, + onSuccess: _proceedToNextStep, + ); + } + break; + case 1: + if (_validateCurrentStep()) { + _proceedToNextStep(); + } + break; + case 2: + if (_validateCurrentStep()) { + _submitReferral(); + } + break; + } + } + + bool _validateCurrentStep() { + FormValidationErrors stepErrors; + + switch (_currentStep) { + case 0: + stepErrors = ReferralValidator.validateStep1(_formManager.formData); + break; + case 1: + stepErrors = ReferralValidator.validateStep2(_formManager.formData); + break; + case 2: + stepErrors = ReferralValidator.validateStep3(_formManager.formData); + break; + default: + stepErrors = FormValidationErrors(); + } + + // Update errors through manager + _formManager.setErrors(stepErrors); + + return !ReferralValidator.hasErrors(stepErrors); + } + + void _proceedToNextStep() { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + // setState(() { + _currentStep++; + // }); + // widget.onStepChanged(_currentStep); + } + + void _submitReferral() { + CreateEReferralRequestModel createReferralRequestModel = CreateEReferralRequestModel( + isInsuredPatient: _formManager.formData.isPatientInsured, + cityCode: _formManager.formData.patientCity!.iD!.toString(), + cityName: _formManager.formData.patientCity!.description, + requesterName: _formManager.formData.requesterName, + requesterContactNo: _formManager.formData.countryEnum.countryCode + _formManager.formData.requesterPhone, + requesterRelationship: _formManager.formData.relationship?.iD, + otherRelationship: _formManager.formData.relationship!.iD.toString(), + fullName: _formManager.formData.patientName, + identificationNo: int.tryParse(_formManager.formData!.patientIdentification ?? '0'), + patientMobileNumber: _formManager.formData.patientCountryEnum.countryCode + _formManager.formData.patientPhone, + preferredBranchCode: _formManager.formData.branch!.iD, + medicalReportAttachment: _formManager.formData.medicalReportImages, + insuranceCardAttachment: _formManager.formData.insuredPatientImages, + preferredBranchName: _formManager.formData.branch!.desciption); + + final hmgServicesVM = context.read(); + LoaderBottomSheet.showLoader(); + + hmgServicesVM.createEReferral( + requestModel: createReferralRequestModel, + onSuccess: (GenericApiModel response) { + showSuccessBottomSheet(int.parse(response.data), hmgServicesVM); + LoaderBottomSheet.hideLoader(); + }, + onError: (errorMessage) { + // Handle error (e.g., show error message) + print(errorMessage); + }, + ); + } + + void _loadData() { + final authVM = context.read(); + final habibWalletVM = context.read(); + final hmgServicesVM = context.read(); + + hmgServicesVM.getRelationshipType(); + authVM.loadCountriesData(); + hmgServicesVM.getAllCities(); + habibWalletVM.getProjectsList(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "E Referral".needTranslation, + isClose: false, + search: () async { + await Navigator.of(context).push( + CustomPageRoute( + page: SearchEReferralPage(), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + }, + bottomChild: Container( + color: Colors.white, + padding: EdgeInsets.all(ResponsiveExtension(20).h), + child: CustomButton( + text: _currentStep <= 1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(), + // icon: AppAssets.search_icon, + iconColor: Colors.white, + onPressed: () => {_handleNextStep()}, + ), + ), + child: ChangeNotifierProvider.value( + value: _formManager, + child: SizedBox( + height: ResponsiveExtension.screenHeight * 0.65, + child: Column( + children: [ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + if (_currentStep == index) { + return StepperWidget(widthOfOneState, AppColors.primaryRedColor, true, 4.h); + } else { + return StepperWidget(widthOfOneState, AppColors.greyLightColor, false, 4.h); + } + })), + Expanded( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + onPageChanged: (index) => { + // setState(() => _currentStep = index) + }, + children: [ + RequesterFormStep(), + PatientInformationStep(), + OtherDetailsStep(), + ], + )), + ], + )), + ))); + // ); + } + + showSuccessBottomSheet(int requestId, HmgServicesViewModel hmgServicesViewModel) { + return showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: "Your Referral has been created Successfully.".needTranslation), + Row( + children: [ + "Here is your Referral #: ".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ("$requestId").toText16(isBold: true), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + context.pop(); + context.pop(); + _currentStep = 0; + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + } +} diff --git a/lib/presentation/e_referral/search_e_referral.dart b/lib/presentation/e_referral/search_e_referral.dart new file mode 100644 index 0000000..1a243da --- /dev/null +++ b/lib/presentation/e_referral/search_e_referral.dart @@ -0,0 +1,119 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/search_e_referral_req_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/widget/search_e_referral_form.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'e_referral_form_manager.dart'; +import 'e_referral_search_result.dart'; + +class SearchEReferralPage extends StatefulWidget { + + const SearchEReferralPage({ + super.key, + }); + + @override + State createState() => _SearchEReferralPageState(); +} + +class _SearchEReferralPageState extends State { + final ReferralFormManager _formManager = ReferralFormManager(); + late HmgServicesViewModel hmgServicesVM; + @override + void initState() { + super.initState(); + _loadData(); + } + + void _handleSearch() { + _formManager.validateSearchForm(); + if (!_formManager.isSearchFormInValid) { + _searchReferral(); + } + } + + void _searchReferral() { + SearchEReferralRequestModel searchEReferralReq; + + if (_formManager.searchCriteria == 0) { + searchEReferralReq = SearchEReferralRequestModel( + identificationNo: _formManager.searchValue, + patientMobileNumber: _formManager.formData.countryEnum.countryCode + _formManager.searchPhone!, + referralNumber: 0, + ); + } else { + + searchEReferralReq = SearchEReferralRequestModel(referralNumber: int.parse(_formManager.searchValue!), patientMobileNumber: _formManager.formData.patientPhone, identificationNo: ''); + } + + hmgServicesVM = context.read(); + LoaderBottomSheet.showLoader(); + hmgServicesVM.searchEReferral( + requestModel: searchEReferralReq, + onSuccess: (response) async { + LoaderBottomSheet.hideLoader(); + await Navigator.of(context).push( + CustomPageRoute( + page: SearchResultPage(), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + }, + + ); + } + + void _loadData() { + WidgetsBinding.instance.addPostFrameCallback((_) {}); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Search E-Referral".needTranslation, + isClose: true, + bottomChild: Container( + color: Colors.white, + padding: EdgeInsets.all(ResponsiveExtension(20).h), + child: CustomButton( + text: LocaleKeys.search.tr(), + icon: AppAssets.search_icon, + iconColor: Colors.white, + onPressed: () => { + _handleSearch() + }, + ), + ), + child: ChangeNotifierProvider.value( + value: _formManager, + child: Column( + children: [ + _buildHeader(), + SearchEReferralFormForm(), + + ], + )), + ); + } + + Widget _buildHeader() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [SizedBox(height: 8), 'Please enter the required information to search for an e-referral'.needTranslation.toText12()], + ), + ); + } + +} diff --git a/lib/presentation/e_referral/widget/e-referral_otp.dart b/lib/presentation/e_referral/widget/e-referral_otp.dart new file mode 100644 index 0000000..390fc71 --- /dev/null +++ b/lib/presentation/e_referral/widget/e-referral_otp.dart @@ -0,0 +1,102 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart'; + +class OTPService { + static void openOTPScreen({ + required BuildContext context, + required ReferralFormManager formManager, + required Function onSuccess, + }) { + _showOTPVerificationSheet(context: context, formManager: formManager, onSuccess: onSuccess); + } + + static void _showOTPVerificationSheet({ + required BuildContext context, + required ReferralFormManager formManager, + required Function onSuccess, + }) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + useSafeArea: true, + backgroundColor: Colors.transparent, + builder: (bottomSheetContext) => Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom), + child: SingleChildScrollView( + child: GenericBottomSheet( + isEnableCountryDropdown: true, + textController: TextEditingController(), + onChange: (value) { + formManager.updateRequesterPhone(value ?? ''); + }, + onCountryChange: (value) { + formManager.updateCountryEnum(value); + }, + autoFocus: true, + buttons: [ + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: CustomButton( + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: () async { + if (ValidationUtils.isValidatePhone( + phoneNumber: formManager.formData.requesterPhone, + onOkPress: () { + Navigator.pop(context); + }, + )) { + Navigator.pop(context); + final hmgServicesViewModel = context.read(); + + LoaderBottomSheet.showLoader(); + hmgServicesViewModel.eReferralSendActivationCode( + requestModel: SendActivationCodeForEReferralRequestModel( + patientMobileNumber: int.parse(formManager.formData.requesterPhone), + zipCode: formManager.formData.countryEnum.countryCode, + patientOutSA: formManager.formData.countryEnum.countryCode == '966' ? 0 : 1, + ), + onSuccess: (GenericApiModel response) { + LoaderBottomSheet.hideLoader(); + hmgServicesViewModel.navigateToOTPScreen( + otpTypeEnum: OTPTypeEnum.sms, + phoneNumber: formManager.formData.requesterPhone, + loginToken: response.data, + onSuccess: () { + Navigator.pop(context); + onSuccess(); + }); + }, + onError: (String errorMessage) { + LoaderBottomSheet.hideLoader(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMessage))); + }, + ); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: AppColors.whiteColor, + icon: AppAssets.message, + ), + ), + ], + ), + )), + ); + } +} diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart new file mode 100644 index 0000000..6efd063 --- /dev/null +++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart @@ -0,0 +1,317 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; + + +class OtherDetailsStep extends StatefulWidget { + const OtherDetailsStep({super.key}); + + @override + State createState() => _OtherDetailsStepState(); +} + +class _OtherDetailsStepState extends State { + final TextEditingController _medicalReportController = TextEditingController(); + final TextEditingController _insuranceController = TextEditingController(); + + late ReferralFormManager _formManager; + + @override + void initState() { + super.initState(); + _formManager = context.read(); + _updateMedicalReportText(); + _updateInsuranceText(); + } + + @override + void didUpdateWidget(OtherDetailsStep oldWidget) { + super.didUpdateWidget(oldWidget); + _updateMedicalReportText(); + _updateInsuranceText(); + } + + void _updateMedicalReportText() { + final hasMedicalReports = _formManager.formData.medicalReportImages.isNotEmpty; + _medicalReportController.text = hasMedicalReports + ? '${_formManager.formData.medicalReportImages.length} file(s) selected'.needTranslation + : ''; + } + + void _updateInsuranceText() { + final hasInsuranceDocs = _formManager.formData.insuredPatientImages.isNotEmpty; + _insuranceController.text = hasInsuranceDocs + ? '${_formManager.formData.insuredPatientImages.length} file(s) selected'.needTranslation + : ''; + + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, formManager, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ListView( + physics: const BouncingScrollPhysics(), + children: [ + const SizedBox(height: 12), + _buildSectionTitle('Other Details'.needTranslation), + const SizedBox(height: 12), + _buildMedicalReportField(formManager), + _buildBranchField(context, formManager), + _buildInsuranceCheckbox(formManager), + if (formManager.formData.isPatientInsured) _buildInsuranceField(formManager), + ], + ), + ); + }, + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16) + ).paddingSymmetrical(4, 0); + } + + Widget _buildMedicalReportField(ReferralFormManager formManager) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + child: TextInputWidget( + controller: _medicalReportController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Medical Report'.needTranslation, + labelText: 'Select Attachment'.needTranslation, + suffix: const Icon(Icons.attachment), + isReadOnly: true, + errorMessage: formManager.errors.medicalReport, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.medicalReport), + ), + onTap: () { + ImageOptions.showImageOptionsNew( + context, + false, + (String image, File file) { + _addMedicalReport(image, file, formManager); + }, + ); + }, + ), + if (formManager.formData.medicalReportImages.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + spacing: 8.0, + children: formManager.formData.medicalReportImages.asMap().entries.map((entry) { + final index = entry.key; + return Chip( + label: Text('Medical Report ${index + 1}'.needTranslation), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () { + _removeMedicalReport(index, formManager); + }, + ); + }).toList(), + ), + ), + ], + ).paddingSymmetrical(0, 4); + } + + Widget _buildBranchField(BuildContext context, ReferralFormManager formManager) { + return DropdownWidget( + labelText: 'Branch', + hintText: formManager.formData.branch?.desciption ?? "Select Branch", + isEnable: false, + hasSelectionCustomIcon: true, + labelColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + selectionCustomIcon: AppAssets.arrow_down, + leadingIcon: AppAssets.hospital, + dropdownItems: [], + errorMessage: formManager.errors.branch, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.branch), + ).paddingSymmetrical(0, 4).onPress(() { + _showBranchBottomSheet(context, formManager); + }); + } + + Widget _buildInsuranceCheckbox(ReferralFormManager formManager) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + Checkbox( + value: formManager.formData.isPatientInsured, + activeColor: Colors.red, + onChanged: (bool? newValue) { + final value = newValue ?? false; + formManager.updateIsPatientInsured(value); + if (!value) { + _updateInsuranceText(); + } + }, + ), + Padding( + padding: EdgeInsets.all(5.0), + child: + "Patient is Insured".needTranslation.toText14( + color: Colors.black, + weight: FontWeight.w600, + ), + // style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + //), + ), + ], + ), + ], + ).paddingSymmetrical(0, 4); + } + + Widget _buildInsuranceField(ReferralFormManager formManager) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + child: TextInputWidget( + controller: _insuranceController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Insurance Document'.needTranslation, + labelText: 'Select Attachment'.needTranslation, + suffix: const Icon(Icons.attachment), + isReadOnly: true, + errorMessage: formManager.errors.insuredDocument, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.insuredDocument), + ), + onTap: () { + ImageOptions.showImageOptionsNew( + context, + false, + (String image, File file) { + _addInsuranceDocument(image, file, formManager); + }, + ); + }, + ), + if (formManager.formData.insuredPatientImages.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + spacing: 8.0, + children: formManager.formData.insuredPatientImages.asMap().entries.map((entry) { + final index = entry.key; + return Chip( + label: Text('Insurance ${index + 1}'), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () { + _removeInsuranceDocument(index, formManager); + }, + ); + }).toList(), + ), + ), + ], + ); + } + + void _showBranchBottomSheet(BuildContext context, ReferralFormManager formManager) { + + showCommonBottomSheetWithoutHeight( + context, + title: "Select Branch".needTranslation, + child: Consumer( + builder: (context, habibWalletVM, child) { + final hospitals = habibWalletVM.advancePaymentHospitals; + if (hospitals == null || hospitals.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No branches available'), + ), + ); + } + + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + customBorder: BorderRadius.all(Radius.circular(24.h)) , + + ), child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.all(16.h), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final branch = hospitals[index]; + return ListTile( + title: (branch.desciption ?? 'Unknown').toText14(), + onTap: () { + formManager.updateBranch(branch); + Navigator.pop(context); + }, + ); + }, + // separatorBuilder: (context, index) => const Divider(), + itemCount: hospitals.length, + )); + }, + ), + useSafeArea: true, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + void _addMedicalReport(String image, File file, ReferralFormManager formManager) { + final newAttachment = EReferralAttachment( + fileName: 'medical_report_${formManager.formData.medicalReportImages.length + 1}.png', + base64String: image + ); + + formManager.addMedicalReport(newAttachment); + _updateMedicalReportText(); + } + + void _removeMedicalReport(int index, ReferralFormManager formManager) { + formManager.removeMedicalReport(index); + _updateMedicalReportText(); + } + + void _addInsuranceDocument(String image, File file, ReferralFormManager formManager) { + final newAttachment = EReferralAttachment( + fileName: 'insurance_${formManager.formData.insuredPatientImages.length + 1}.png', + base64String: image + ); + + formManager.addInsuranceDocument(newAttachment); + _updateInsuranceText(); + } + + void _removeInsuranceDocument(int index, ReferralFormManager formManager) { + formManager.removeInsuranceDocument(index); + _updateInsuranceText(); + } + + @override + void dispose() { + _medicalReportController.dispose(); + _insuranceController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/presentation/e_referral/widget/e_referral_patient_info.dart b/lib/presentation/e_referral/widget/e_referral_patient_info.dart new file mode 100644 index 0000000..469755c --- /dev/null +++ b/lib/presentation/e_referral/widget/e_referral_patient_info.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; + +class PatientInformationStep extends StatefulWidget { + const PatientInformationStep({super.key}); + + @override + State createState() => PatientInformationStepState(); +} + +class PatientInformationStepState extends State { + late TextEditingController _identificationController; + late TextEditingController _nameController; + late TextEditingController _phoneController; + + late FocusNode _identificationFocusNode; + late FocusNode _nameFocusNode; + late FocusNode _phoneFocusNode; + + late ReferralFormManager _formManager; + + @override + void initState() { + super.initState(); + _formManager = context.read(); + + _identificationController = TextEditingController(); + _nameController = TextEditingController(); + _phoneController = TextEditingController(); + + _identificationFocusNode = FocusNode(); + _nameFocusNode = FocusNode(); + _phoneFocusNode = FocusNode(); + + // Initialize controllers with current values + _identificationController.text = _formManager.formData.patientIdentification ?? ''; + _nameController.text = _formManager.formData.patientName ?? ''; + _phoneController.text = _formManager.formData.patientPhone ?? ''; + + // Auto-focus the identification field when the step loads + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _identificationFocusNode.requestFocus(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, formManager, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ListView( + physics: const BouncingScrollPhysics(), + children: [ + const SizedBox(height: 12), + _buildSectionTitle('Patient information'.needTranslation), + const SizedBox(height: 12), + _buildIdentificationField(formManager), + _buildPatientNameField(formManager), + // _buildPatientCountryField(context, formManager), + _buildPatientPhoneField(formManager), + const SizedBox(height: 20), + _buildSectionTitle('Where the patient located'.needTranslation), + _buildPatientCityField(context, formManager), + ], + ), + ); + }, + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16) + ).paddingSymmetrical(4, 0); + } + + Widget _buildIdentificationField(ReferralFormManager formManager) { + return Focus( + focusNode: _identificationFocusNode, + child: TextInputWidget( + controller: _identificationController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Enter Identification Number*'.needTranslation, + labelText: 'Identification Number'.needTranslation, + errorMessage: formManager.errors.patientIdentification, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientIdentification), + onChange: (value) { + formManager.updatePatientIdentification(value ?? ''); + }, + onSubmitted: (value) { + _nameFocusNode.requestFocus(); + }, + ).paddingSymmetrical(0, 4), + ); + } + + Widget _buildPatientNameField(ReferralFormManager formManager) { + return Focus( + focusNode: _nameFocusNode, + child: TextInputWidget( + controller: _nameController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Patient Name*'.needTranslation, + labelText: 'Name'.needTranslation, + keyboardType: TextInputType.text, + errorMessage: formManager.errors.patientName, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientName), + onChange: (value) { + formManager.updatePatientName(value ?? ''); + }, + onSubmitted: (value) { + // Optionally move to next field or keep focus + }, + ).paddingSymmetrical(0, 4), + ); + } + + Widget _buildPatientPhoneField(ReferralFormManager formManager) { + return Focus( + focusNode: _phoneFocusNode, + child: TextInputWidget( + labelText: 'Phone Number'.needTranslation, + hintText: "5xxxxxxxx", + controller: _phoneController, + padding: const EdgeInsets.all(8), + keyboardType: TextInputType.number, + onChange: (value) { + formManager.updatePatientPhone(value ?? ''); + }, + onCountryChange: (value) { + formManager.updatePatientCountryEnum(value); + }, + prefix: '966', + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 13, + isCountryDropDown: true, + leadingIcon: AppAssets.smart_phone, + errorMessage: formManager.errors.patientPhone, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientPhone) + ).paddingSymmetrical(0, 8), + ); + } + + Widget _buildPatientCityField(BuildContext context, ReferralFormManager formManager) { + return DropdownWidget( + labelText: 'City', + hintText: formManager.formData.patientCity?.description ?? "Select City".needTranslation, + isEnable: false, + hasSelectionCustomIcon: true, + labelColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + selectionCustomIcon: AppAssets.arrow_down, + leadingIcon: AppAssets.globe, + dropdownItems: [], + errorMessage: formManager.errors.patientCity, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientCity), + ).paddingSymmetrical(0, 4).onPress(() { + _showCityBottomSheet(context, formManager); + }); + } + + + void _showCityBottomSheet(BuildContext context, ReferralFormManager formManager) { + + showCommonBottomSheetWithoutHeight( + context, + title: "Select City".needTranslation, + child: Consumer( + builder: (context, hmgServicesVM, child) { + final cities = hmgServicesVM.getAllCitiesList; + if (cities == null || cities.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No cities available'), + ), + ); + } + + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + customBorder: BorderRadius.all(Radius.circular(24.h)) , + + ), child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.all(16.h), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final city = cities[index]; + return ListTile( + title: (city.description ?? 'Unknown').toText14(), + onTap: () { + formManager.updatePatientCity(city); + Navigator.pop(context); + }, + ); + }, + // separatorBuilder: (context, index) => const Divider(), + itemCount: cities.length, + )); + }, + ), + useSafeArea: true, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + @override + void dispose() { + _identificationController.dispose(); + _nameController.dispose(); + _phoneController.dispose(); + _identificationFocusNode.dispose(); + _nameFocusNode.dispose(); + _phoneFocusNode.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/presentation/e_referral/widget/e_referral_requester_form.dart b/lib/presentation/e_referral/widget/e_referral_requester_form.dart new file mode 100644 index 0000000..cc981a9 --- /dev/null +++ b/lib/presentation/e_referral/widget/e_referral_requester_form.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; + +class RequesterFormStep extends StatefulWidget { + const RequesterFormStep({super.key}); + + @override + State createState() => RequesterFormStepState(); +} + +class RequesterFormStepState extends State { + late TextEditingController _nameController; + late TextEditingController _phoneController; + late TextEditingController _otherNameController; + + late FocusNode _nameFocusNode; + late FocusNode _phoneFocusNode; + late FocusNode _otherNameFocusNode; + + late ReferralFormManager _formManager; + + @override + void initState() { + super.initState(); + _formManager = context.read(); + + _nameController = TextEditingController(); + _phoneController = TextEditingController(); + _otherNameController = TextEditingController(); + + _nameFocusNode = FocusNode(); + _phoneFocusNode = FocusNode(); + _otherNameFocusNode = FocusNode(); + + // Initialize controllers with current values + _nameController.text = _formManager.formData.requesterName ?? ''; + _phoneController.text = _formManager.formData.requesterPhone ?? ''; + _otherNameController.text = _formManager.formData.otherRelationshipName ?? ''; + + // Auto-focus the name field when the step loads + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFocusNode.requestFocus(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, formManager, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 0.0), + child: ListView( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + children: [ + // const SizedBox(height: 12), + _buildSectionTitle('Referral requester information'.needTranslation), + const SizedBox(height: 12), + _buildNameField(formManager), + // _buildPhoneField(formManager), + _buildRelationshipField(context, formManager), + if (_showOtherNameField(formManager)) _buildOtherNameField(formManager), + ], + ), + ); + }, + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16) + ).paddingSymmetrical(4, 0); + } + + Widget _buildNameField(ReferralFormManager formManager) { + return Focus( + focusNode: _nameFocusNode, + child: TextInputWidget( + controller: _nameController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Enter Referral Requester Name*'.needTranslation, + labelText: 'Requester Name'.needTranslation, + keyboardType: TextInputType.text, + errorMessage: formManager.errors.requesterName, + isAllowLeadingIcon: true, + leadingIcon: AppAssets.user_circle, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.requesterName), + onChange: (value) { + formManager.updateRequesterName(value ?? ''); + }, + + ).paddingSymmetrical(0, 8), + ); + } + Widget _buildRelationshipField(BuildContext context, ReferralFormManager formManager) { + return DropdownWidget( + labelText: "Relationship".needTranslation, + hintText: formManager.formData.relationship?.textEn ?? "Select Relation".needTranslation, + isEnable: false, + selectedValue: formManager.formData.relationship?.textEn ?? "Select Relation".needTranslation, + errorMessage: formManager.errors.relationship, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.relationship), + hasSelectionCustomIcon: false, + labelColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + leadingIcon: AppAssets.user_circle, + dropdownItems: [], + ).paddingSymmetrical(0, 8).onPress(() { + _showRelationshipBottomSheet(context, formManager); + }); + } + + Widget _buildOtherNameField(ReferralFormManager formManager) { + return Focus( + focusNode: _otherNameFocusNode, + child: TextInputWidget( + controller: _otherNameController, + keyboardType: TextInputType.text, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: 'Other Name*'.needTranslation, + labelText: 'Other Name'.needTranslation, + errorMessage: formManager.errors.otherRelationshipName, + onChange: (value) { + formManager.updateOtherRelationshipName(value ?? ''); + }, + ).paddingSymmetrical(0, 4), + ); + } + + bool _showOtherNameField(ReferralFormManager formManager) { + return formManager.formData.relationship != null && + formManager.formData.relationship?.iD == 5; + } + + void _showRelationshipBottomSheet(BuildContext context, ReferralFormManager formManager) { + final hmgServicesVM = context.read(); + + showCommonBottomSheetWithoutHeight( + context, + title: "Select Relation".needTranslation, + child: Consumer( + builder: (context, hmgServicesVM, child) { + if (hmgServicesVM.relationTypes.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No relationships available'), + ), + ); + } + + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + customBorder: BorderRadius.all(Radius.circular(24.h)) , + + ), child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.all(16.h), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final relationship = hmgServicesVM.relationTypes[index]; + return ListTile( + title:relationship.textEn?.toText14(), + onTap: () { + formManager.updateRelationship(relationship); + Navigator.pop(context); + }, + ); + }, + //separatorBuilder: (context, index) => const Divider(), + itemCount: hmgServicesVM.relationTypes.length, + )); + }, + ), + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + @override + void dispose() { + _nameController.dispose(); + _phoneController.dispose(); + _otherNameController.dispose(); + _nameFocusNode.dispose(); + _phoneFocusNode.dispose(); + _otherNameFocusNode.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/presentation/e_referral/widget/search_e_referral_form.dart b/lib/presentation/e_referral/widget/search_e_referral_form.dart new file mode 100644 index 0000000..fa3ae19 --- /dev/null +++ b/lib/presentation/e_referral/widget/search_e_referral_form.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; + +class SearchEReferralFormForm extends StatefulWidget { + final VoidCallback? onFormValidated; + const SearchEReferralFormForm({super.key, this.onFormValidated}); + + @override + State createState() => SearchEReferralFormFormState(); +} + +class SearchEReferralFormFormState extends State { + late TextEditingController _searchController; + late TextEditingController _phoneController; + late FocusNode _searchFocusNode; + late FocusNode _phoneFocusNode; + + @override + void initState() { + super.initState(); + + _searchController = TextEditingController(); + _phoneController = TextEditingController(); + _searchFocusNode = FocusNode(); + _phoneFocusNode = FocusNode(); + + // Initialize controllers with current values from form manager + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + final formManager = context.read(); + _searchController.text = formManager.searchValue ?? ''; + _phoneController.text = formManager.searchPhone ?? ''; + // _searchFocusNode.requestFocus(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, formManager, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + // const SizedBox(height: 12), + _buildSelectionField(context, formManager), + _buildNameField(formManager), + _buildPhoneField(formManager), + ], + ), + ); + }, + ); + } + + Widget _buildNameField(ReferralFormManager formManager) { + return Focus( + focusNode: _searchFocusNode, + autofocus: true, + child: TextInputWidget( + controller: _searchController, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + hintText: formManager.searchCriteria == 0 ? "Enter Identification Number" : "Enter Referral Number", + labelText: formManager.searchCriteria == 0 ? "Identification Number" : "Referral Number", + keyboardType: TextInputType.number, + errorMessage: formManager.errors.searchValue, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.searchValue), + onChange: (value) { + formManager.updateSearchValue(value ?? ''); + //_validateForm(formManager); + }, + ).paddingSymmetrical(0, 8), + ); + } + + Widget _buildPhoneField(ReferralFormManager formManager) { + return Focus( + focusNode: _phoneFocusNode, + autofocus: false, + child: TextInputWidget( + autoFocus: false, + labelText: 'Phone Number', + hintText: "5xxxxxxxx", + controller: _phoneController, + padding: const EdgeInsets.all(8), + keyboardType: TextInputType.number, + onChange: (value) { + formManager.updateSearchPhone(value ?? ''); + // _validateForm(formManager); + }, + // onSubmitted: (value) { + // formManager.updateRequesterPhone(value ?? ''); + // _validateForm(formManager); + // }, + onCountryChange: (value) { + formManager.updateCountryEnum(value); + // _validateForm(formManager); + }, + prefix: '966', + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 13, + isCountryDropDown: true, + leadingIcon: AppAssets.smart_phone, + errorMessage: formManager.errors.searchPhone, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.searchPhone), + ).paddingSymmetrical(0, 8), + ); + } + + Widget _buildSelectionField(BuildContext context, ReferralFormManager formManager) { + return DropdownWidget( + labelText: "Select the Search Criteria", + hintText: formManager.searchCriteria == 0 ? "Identification Number" : "Referral Number", + isEnable: false, + hasSelectionCustomIcon: false, + labelColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + leadingIcon: AppAssets.search_icon, + dropdownItems: [], + ).paddingSymmetrical(0, 8).onPress(() { + _showCriteriaBottomSheet(context, formManager); + }); + } + + void _showCriteriaBottomSheet(BuildContext context, ReferralFormManager formManager) { + final criteriaList = [ + {0: 'Identification Number'}, + {1: 'Referral Number'}, + ]; + + showCommonBottomSheetWithoutHeight( + context, + title: "Select Criteria", + child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final criteria = criteriaList[index]; + final criteriaKey = criteria.keys.first; + final criteriaValue = criteria.values.first; + + return ListTile( + leading: Radio( + value: criteriaKey, + groupValue: formManager.searchCriteria, + onChanged: (value) { + if (value != null) { + formManager.updateSearchCriteria(value); + _searchController.clear(); + _validateForm(formManager); + Navigator.pop(context); + } + }, + ), + title: Text(criteriaValue), + onTap: () { + formManager.updateSearchCriteria(criteriaKey); + _searchController.clear(); + _validateForm(formManager); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: criteriaList.length, + ), + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + void _validateForm(ReferralFormManager formManager) { + // Trigger validation + formManager.validateSearchForm(); + + // Notify parent if form validation state changes + widget.onFormValidated?.call(); + } + + @override + void dispose() { + _searchController.dispose(); + _phoneController.dispose(); + _searchFocusNode.dispose(); + _phoneFocusNode.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/emergency_services/RRT/rrt_map_screen.dart b/lib/presentation/emergency_services/RRT/rrt_map_screen.dart new file mode 100644 index 0000000..afe68b6 --- /dev/null +++ b/lib/presentation/emergency_services/RRT/rrt_map_screen.dart @@ -0,0 +1,630 @@ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; +import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; +import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/AddressItem.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart' show AppointmentBottomSheet; +import 'package:hmg_patient_app_new/presentation/emergency_services/widgets/location_input_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/CustomSwitch.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart'; +import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/model/BottomSheetType.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/map/HMSMap.dart'; +import 'package:hmg_patient_app_new/widgets/map/gms_map.dart'; +import 'package:provider/provider.dart'; + +import '../../../widgets/common_bottom_sheet.dart'; + +class RrtMapScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: Visibility( + visible: context.watch().bottomSheetType == + BottomSheetType.FIXED, + child: Padding( + padding: EdgeInsetsDirectional.only(end: 8.h, bottom: 68.h), + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 12.h), + child: Utils.buildSvgWithAssets( + icon: AppAssets.locate_me, width: 24.h, height: 24.h) + .paddingAll(12.h) + .onPress(() { + context + .read() + .moveToCurrentLocation(); + }), + ), + ), + ), + bottomSheet: ExpandableBottomSheet( + bottomSheetType: + context.watch().bottomSheetType, + children: { + BottomSheetType.EXPANDED: ExpanedBottomSheet(context), + BottomSheetType.FIXED: FixedBottomSheet(context), + }, + ), + body: Stack( + children: [ + if (context.read().isGMSAvailable) + GMSMap( + currentLocation: + context.read().getGMSLocation(), + onCameraMoved: (value) => context + .read() + .handleGMSMapCameraMoved(value), + onCameraIdle: + context.read().handleOnCameraIdle, + myLocationEnabled: true, + inputController: + context.read().gmsController, + showCenterMarker: true, + ) + else + HMSMap( + currentLocation: + context.read().getHMSLocation(), + onCameraMoved: (value) => context + .read() + .handleHMSMapCameraMoved(value), + onCameraIdle: + context.read().handleOnCameraIdle, + myLocationEnabled: false, + inputController: + context.read().hmsController, + showCenterMarker: true, + ), + Align( + alignment: AlignmentDirectional.topStart, + child: Utils.buildSvgWithAssets( + icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h) + .onPress(() { + context + .read() + .flushPickupInformation(); + Navigator.pop(context); + }), + ).paddingOnly(top: 51.h, left: 24.h), + ], + ), + ); + } + + Widget FixedBottomSheet(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: (details){ + }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 24.h, + children: [ + inputFields(context).paddingSymmetrical(16.h, 0.h), + SizedBox( + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24.h, + children: [ + Column( + spacing: 4.h, + children: [ + "Select Location".needTranslation.toText21( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + "Please select the location".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ) + ], + ), + CustomButton( + text: "Submit Request".needTranslation, + onPressed: () { + LocationViewModel locationViewModel = context.read(); + GeocodeResponse? response = locationViewModel.geocodeResponse; + PlaceDetails? placeDetails = locationViewModel.placeDetails; + PlacePrediction? placePrediction = locationViewModel.selectedPrediction; + context.read().submitRRTRequest(response?.results.first, placeDetails, placePrediction); + }, + ) + ], + ).paddingOnly(top: 24.h, bottom: 32.h, left: 24.h, right: 24.h), + ), + ), + ], + ), + ], + ), + ); + } + + Widget ExpanedBottomSheet(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: (details){ + if(details.delta.dy>0){ + context.read().updateBottomSheetState(BottomSheetType.FIXED); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + ), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [ + + hospitalAndPickUpSection(context), + + + ], + ).paddingOnly(top: 24.h, bottom: 32.h,left: 24.h, right: 24.h), + + bottomPriceContent(context) + ], + ), + ), + ], + ), + ); + } + + locationsSections(BuildContext context) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + ListView.separated( + separatorBuilder: (_, __) => Column( + children: [ + SizedBox(height: 16.h), + Divider( + color: AppColors.bottomNAVBorder, + height: 1, + thickness: 1, + ), + SizedBox(height: 16.h), + ], + ), + shrinkWrap: true, + itemCount: 3, + itemBuilder: (__, index) { + if (index == + 2) // todo means the end of the list so handle as per the viewmodel + { + return CustomButton( + height: 40.h, + backgroundColor: AppColors.lightRedButtonColor, + borderColor: Colors.transparent, + text: "Add new address".needTranslation, + textColor: AppColors.primaryRedColor, + iconColor: AppColors.primaryRedColor, + onPressed: () {}, + icon: AppAssets.add_icon); + } else { + return AddressItem( + isSelected: index == 0, + address: + "Flat No 301, Building No 12, Palm Spring Apartment, Sector 45, Gurugram, Haryana 122003", + title: index == 0 + ? "Home".needTranslation + : "Work".needTranslation, + onTap: () {}, + ); + } + }) + ]).paddingAll(16.h), + ); + } + + hospitalAndPickUpSection(BuildContext context) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [ + // Row( + // children: [ + // hospitalAndPickUpItemContent( + // title: "Select Hospital".needTranslation, + // subTitle: "hospital".needTranslation, + // leadingIcon: AppAssets.hospital, + // ), + // Utils.buildSvgWithAssets(icon: AppAssets.down_cheveron, + // width: 24.h, height: 24.h) + // .paddingAll(16.h) + // ], + // ).onPress((){ + // showHospitalBottomSheet(context); + // }), + // SizedBox(height: 16.h), + // Divider( + // color: AppColors.bottomNAVBorder, + // height: 1, + // thickness: 1, + // ), + // SizedBox(height: 16.h), + + Row( + children: [ + hospitalAndPickUpItemContent( + title: "Pick".needTranslation, + subTitle: "Inside the home".needTranslation, + leadingIcon: AppAssets.pickup_bed, + ), + CustomSwitch( + value: context + .watch() + .pickupFromInsideTheLocation, + onChanged: (value){ + context + .read() + .updateThePickupPlaceFromLocation(value); + }, + ) + ], + ), + + Row( + children: [ + hospitalAndPickUpItemContent( + title: '', + subTitle: "Have any appointment".needTranslation, + leadingIcon: AppAssets.appointment_checkin_icon, + ), + CustomSwitch( + value: context + .watch() + .haveAnAppointment, + onChanged: (value) async { + // if (value) { + // openAppointmentList(context); + // } + await context.read() + .updateAppointment(value); + if (context.read().appointments?.isNotEmpty == true) { + openAppointmentList(context); + } + }, + ) + ], + ), + ], + ).paddingAll(16.h), + ); + } + + leadingIcon(String leadingIcon) { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.only(right: 10.h), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.h, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: leadingIcon), + ); + } + + hospitalAndPickUpItemContent({ + required String title, + required String subTitle, + required String leadingIcon, + }) { + return Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + this.leadingIcon(leadingIcon), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Visibility( + visible: title.isNotEmpty, + child: Column( + children: [ + title.toText12( + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + SizedBox(height: 2.h), + ], + ), + ), + subTitle.toText14( + color: AppColors.textColor, + weight: FontWeight.w500, + ), + ], + ), + ), + ], + ), + ); + } + + bottomPriceContent(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + hasShadow: true + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12.h, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.h, + children: [ + "Total amount to pay".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.warning, + height: 18.h, width: 18.h), + SizedBox(width: 4.h,), + "Amount will be paid at the hospital" + .needTranslation + .toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ) + ], + ), + ), + Utils.getPaymentAmountWithSymbol( + (Utils.formatNumberToInternationalFormat(context + .read() + .getTotalPrice() ?? + 0)) + .toText24( + fontWeight: FontWeight.w600, + color: AppColors.textColor, + letterSpacing: -2), + AppColors.blackColor, + 17.h) + + // Utils.getPaymentAmountWithSymbol2(context.read().selectedTransportOption?.priceTotal??"0", letterSpacing: -2) + ], + ), + CustomButton( + text: "Submit Request".needTranslation, + onPressed: () { + LocationViewModel locationViewModel = context.read(); + GeocodeResponse? response = locationViewModel.geocodeResponse; + PlaceDetails? placeDetails = locationViewModel.placeDetails; + PlacePrediction? placePrediction = locationViewModel.selectedPrediction; + context.read().submitAmbulanceRequest(response?.results.first, placeDetails, placePrediction); + }) + ], + ).paddingOnly(top: 24.h, bottom: 12.h, left: 24.h, right: 24.h), + ), + ], + ); + } + + showHospitalBottomSheet(BuildContext context){ + showCommonBottomSheetWithoutHeight( + title: + LocaleKeys.selectHospital.tr(), + context, + child: Consumer( + builder:(_,vm,__)=> HospitalBottomSheetBody( + searchText: vm.searchController, + displayList: vm.displayList, + onFacilityClicked: (value) { + vm.setSelectedFacility(value); + vm.getDisplayList(); + }, + onHospitalClicked: (hospital) { + Navigator.pop(context); + vm.setSelectedHospital(hospital); + }, + onHospitalSearch: (value) { + vm.searchHospitals(value ?? ""); + }, + selectedFacility: + vm.selectedFacility, + hmcCount: vm.hmcCount, + hmgCount: vm.hmgCount, + ), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () { + context.read().clearSearchText(); + }, + ); + } + + ///it will show the places field first and then hospital field + PlaceFirstThanHospitalField(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [ + textPlaceInput(context), + hospitalField(context), + ], + ).paddingOnly(right: 24.h, left: 24.h); + } + + HospitalFieldFirstThanPlaces(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [hospitalField(context), textPlaceInput(context)], + ).paddingOnly(right: 24.h, left: 24.h); + } + + textPlaceInput(context) { + return Consumer(builder: (_, vm, __) { + print( + "the data is ${vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description}"); + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: TextInputWidget( + labelText: "Enter Pickup Location Manually".needTranslation, + hintText: "Enter Pickup Location".needTranslation, + controller: TextEditingController( + text: vm.geocodeResponse?.results.first.formattedAddress ?? + vm.selectedPrediction?.description, + ), + leadingIcon: AppAssets.location_pickup, + isAllowLeadingIcon: true, + isEnable: false, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ).onPress(() { + openLocationInputBottomSheet(context); + }), + ); + }); + } + + ///decide which field to show first based on the selected calling place + Widget inputFields(BuildContext context) { + return textPlaceInput(context); + } + + openLocationInputBottomSheet(BuildContext context) { + context.read().flushSearchPredictions(); + showCommonBottomSheetWithoutHeight( + title: "".needTranslation, + context, + child: SizedBox( + height: MediaQuery.sizeOf(context).height * .8, + child: LocationInputBottomSheet(), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + } + + hospitalField(BuildContext context) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 12.h), + child: Row( + children: [ + hospitalAndPickUpItemContent( + title: "Select Hospital".needTranslation, + subTitle: context + .read() + .getSelectedHospitalName() ?? + "Select Hospital".needTranslation, + leadingIcon: AppAssets.hospital, + ), + Utils.buildSvgWithAssets( + icon: AppAssets.down_cheveron, width: 24.h, height: 24.h) + .paddingAll(16.h) + ], + ).onPress(() { + print("the item is clicked"); + showHospitalBottomSheet(context); + }).paddingSymmetrical( + 10.w, + 12.h, + ), + ); + } + + void openAppointmentList(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: "Select Appointment".needTranslation, + context, + child: SizedBox( + height: MediaQuery.sizeOf(context).height * .5, + child: AppointmentBottomSheet( + list: context.read().appointments!, + onAppointmentSelection: (appointment) { + Navigator.pop(context); + context.read().setSelectedAppointment(appointment); + }), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () { + context.read().checkHasAppointment(); + }, + ); + // ); + } +} diff --git a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart index 0e23d49..13e0106 100644 --- a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart +++ b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart @@ -1,10 +1,15 @@ -import 'package:flutter/cupertino.dart'; +import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/rrt_procedures_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/radio/custom_radio_button.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:provider/provider.dart'; class RrtRequestTypeSelect extends StatelessWidget { @@ -12,35 +17,260 @@ class RrtRequestTypeSelect extends StatelessWidget { @override Widget build(BuildContext context) { - // return Consumer(builder: (context, emergencyServicesVM, child) { + return Consumer(builder: (context, emergencyServicesVM, child) { + print("the checkbox is ${emergencyServicesVM.agreedToTermsAndCondition}"); + return Column( + children: [ + Column( + children: [ + "Rapid Response Team (RRT) options".needTranslation.toText20(color: AppColors.textColor, isBold: true), + SizedBox( + height: 16.h, + ), + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + borderRadius: 20.r, + ), + child: OptionSelection(context)), + SizedBox( + height: 24.h, + ), + termsAndCondition(context, emergencyServicesVM), + ], + ).paddingSymmetrical(24.w, 0.h), + bottomPriceContent(context, emergencyServicesVM) + ], + ); + }); + } + + bottomPriceContent(BuildContext context, EmergencyServicesViewModel emergencyServicesVM) { + if (emergencyServicesVM.selectedRRTProcedure == null) return SizedBox.shrink(); return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Container( - padding: EdgeInsets.all(16.h), - height: 200.h, + DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - ), + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + hasShadow: true), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12.h, children: [ Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - CustomRadioOption( - value: "", - groupValue: "", - onChanged: (value) {}, - text: "Home Visit Emergency", + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12.h, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Total amount to pay".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + Utils.getPaymentAmountWithSymbol( + (Utils.formatNumberToInternationalFormat(context.read().selectedRRTProcedure?.patientShare ?? 0)) + .toText24(fontWeight: FontWeight.w600, color: AppColors.textColor, letterSpacing: -2), + AppColors.blackColor, + 17.h) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.warning, height: 18.h, width: 18.h), + SizedBox( + width: 4.h, + ), + "Amount will be paid at the hospital".needTranslation.toText11( + color: AppColors.greyTextColor, + ), + ], + ), + Row( + children: [ + "+ VAT 15%(".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + "${emergencyServicesVM.selectedRRTProcedure?.patientTaxAmount})".needTranslation.toText14( + weight: FontWeight.w600, + color: AppColors.greyTextColor, + ), + ], + ), + ], + ) + ], + ), + ), + + // Utils.getPaymentAmountWithSymbol2(context.read().selectedTransportOption?.priceTotal??"0", letterSpacing: -2) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 6.w, + children: [ + Image.asset(AppAssets.mada, width: 24.h, height: 24.h), + Image.asset(AppAssets.visa, width: 24.h, height: 24.h), + Image.asset(AppAssets.mastercard, width: 24.h, height: 24.h), + Image.asset(AppAssets.applePay, width: 24.h, height: 24.h), + ], + ), + Column( + children: [ + Divider( + color: AppColors.dividerColor, + thickness: 1.h, + ), + Utils.getPaymentAmountWithSymbol( + (Utils.formatNumberToInternationalFormat(context.read().selectedRRTProcedure?.patientShareWithTax ?? 0)) + .toText24(fontWeight: FontWeight.w600, color: AppColors.textColor, letterSpacing: -2), + AppColors.blackColor, + 17.h), + ], ) ], ), + CustomButton(text: LocaleKeys.next.tr(), onPressed: () { + Navigator.pop(context); + emergencyServicesVM.openRRT(); + }) ], - ), + ).paddingAll(24.h), ), - SizedBox(height: 32.h), ], ); - // }); + } + + Widget OptionSelection(BuildContext context) { + return Selector?, RRTProceduresResponseModel?)>( + // Select both the list and the currently selected procedure + selector: (context, viewModel) => (viewModel.RRTProceduresList, viewModel.selectedRRTProcedure), + builder: (context, data, child) { + final procedureList = data.$1; + final selectedProcedure = data.$2; + + // Handle loading or empty state + if (procedureList == null || procedureList.isEmpty) { + return const SizedBox.shrink(); + } + + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), + // Important for ListView inside a Column + physics: const NeverScrollableScrollPhysics(), + // If the parent is scrollable + itemCount: procedureList.length, + separatorBuilder: (_, __) => Divider(thickness: 1.h, color: AppColors.dividerColor), + itemBuilder: (_, index) { + final procedure = procedureList[index]; + + final indexOfSelectedItem = selectedProcedure != null ? procedureList.indexOf(selectedProcedure) : -1; + return RadioListTile( + title: (procedure.procedureName ?? "").toText16(color: AppColors.textColor, weight: FontWeight.w500), + value: index, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppColors.primaryRedColor; + } + return Color(0xffEEEEEE); + }), + contentPadding: EdgeInsets.zero, + groupValue: indexOfSelectedItem, + onChanged: (int? newValue) { + context.read().setSelectedRRTProcedure(procedure); + }, + ); + // return Row( + // children: [ + // Radio( + // // Specify the type for the Radio button + // value: procedure.procedureID ?? "", + // groupValue: selectedProcedure?.procedureID, + // // Compare with the selected procedure's ID + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // onChanged: (value) { + // // The onChanged of the Radio button should handle the state update + // context.read().setSelectedRRTProcedure(procedure); + // }, + // ), + // Expanded( + // // Use Expanded to allow text to wrap if it's too long + // child: (procedure.procedureName ?? "").toText12(color: AppColors.textColor), + // ) + // ], + }, + ); + }, + ); + } + + termsAndCondition( + BuildContext context, + EmergencyServicesViewModel emergencyServicesVM, + ) { + return Row( + children: [ + SizedBox( + height: 18.h, + width: 18.w, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: emergencyServicesVM.agreedToTermsAndCondition, + checkColor: AppColors.whiteColor, + fillColor: MaterialStateProperty.resolveWith((Set states) { + print("the state is ${states}"); + if (states.contains(WidgetState.selected)) { + return AppColors.errorColor; + } + return AppColors.whiteColor; + }), + onChanged: (value) { + emergencyServicesVM.setTermsAndConditions(value ?? false); + }), + ), + Row( + spacing: 4.w, + children: [ + SizedBox.shrink(), + LocaleKeys.agreeTo.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), + LocaleKeys.termsConditoins.tr().toText16(color: AppColors.errorColor, isUnderLine: true, weight: FontWeight.w500).onPress(() { + emergencyServicesVM.getTermsAndConditions(); + }), + ], + ), + ], + ); + + // CheckboxListTile( + // title: Row( + // children: [ + // LocaleKeys.agreeTo.tr().toText12(color: AppColors.textColor), + // LocaleKeys.termsConditoins.tr().toText12(color: AppColors.errorColor, isUnderLine: true), + // ], + // ), + // value: emergencyServicesVM.agreedToTermsAndCondition, + // onChanged: (value) { + // emergencyServicesVM.setTermsAndConditions(value ?? false); + // }, + // ); } } diff --git a/lib/presentation/emergency_services/RRT/terms_and_condition.dart b/lib/presentation/emergency_services/RRT/terms_and_condition.dart new file mode 100644 index 0000000..1d1dac1 --- /dev/null +++ b/lib/presentation/emergency_services/RRT/terms_and_condition.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +class TermsAndCondition extends StatelessWidget { + final String termsAndCondition; + const TermsAndCondition({super.key, required this.termsAndCondition}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + + Expanded( + child: CollapsingListView( + title: "Terms And Condition".needTranslation, + child:DecoratedBox(decoration:RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ),child: HtmlWidget(termsAndCondition).paddingAll(16.h)).paddingAll(12.h)))])); + } + +} \ No newline at end of file diff --git a/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart b/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart index d0d7c82..043c231 100644 --- a/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart +++ b/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -14,10 +12,7 @@ import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/requesting_services_page.dart' show RequestingServicesPage; -import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/tracking_screen.dart' show TrackingScreen; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart' show AppointmentBottomSheet; import 'package:hmg_patient_app_new/presentation/emergency_services/widgets/location_input_bottom_sheet.dart'; @@ -28,8 +23,7 @@ import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/ExpandableBo import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/model/BottomSheetType.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/map/HMSMap.dart'; -import 'package:hmg_patient_app_new/widgets/map/map.dart'; -import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/map/gms_map.dart'; import 'package:provider/provider.dart'; import '../../../widgets/common_bottom_sheet.dart'; @@ -40,20 +34,13 @@ class CallAmbulancePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( floatingActionButton: Visibility( - visible: context.watch().bottomSheetType == - BottomSheetType.FIXED, + visible: context.watch().bottomSheetType == BottomSheetType.FIXED, child: Padding( padding: EdgeInsetsDirectional.only(end: 8.h, bottom: 68.h), child: DecoratedBox( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, borderRadius: 12.h), - child: Utils.buildSvgWithAssets( - icon: AppAssets.locate_me, width: 24.h, height: 24.h) - .paddingAll(12.h) - .onPress(() { - context - .read() - .moveToCurrentLocation(); + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Utils.buildSvgWithAssets(icon: AppAssets.locate_me, width: 24.h, height: 24.h).paddingAll(12.h).onPress(() { + context.read().moveToCurrentLocation(); }), ), ), @@ -62,8 +49,7 @@ class CallAmbulancePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ExpandableBottomSheet( - bottomSheetType: - context.watch().bottomSheetType, + bottomSheetType: context.watch().bottomSheetType, children: { BottomSheetType.EXPANDED: ExpanedBottomSheet(context), BottomSheetType.FIXED: FixedBottomSheet(context), @@ -73,42 +59,28 @@ class CallAmbulancePage extends StatelessWidget { ), body: Stack( children: [ - if (context.read().isGMSAvailable ) + if (context.read().isGMSAvailable) GMSMap( - currentLocation: - context.read().getGMSLocation(), - onCameraMoved: (value) => context - .read() - .handleGMSMapCameraMoved(value), - onCameraIdle: - context.read().handleOnCameraIdle, + currentLocation: context.read().getGMSLocation(), + onCameraMoved: (value) => context.read().handleGMSMapCameraMoved(value), + onCameraIdle: context.read().handleOnCameraIdle, myLocationEnabled: true, - inputController: - context.read().gmsController, + inputController: context.read().gmsController, showCenterMarker: true, ) else HMSMap( - currentLocation: - context.read().getHMSLocation(), - onCameraMoved: (value) => context - .read() - .handleHMSMapCameraMoved(value), - onCameraIdle: - context.read().handleOnCameraIdle, + currentLocation: context.read().getHMSLocation(), + onCameraMoved: (value) => context.read().handleHMSMapCameraMoved(value), + onCameraIdle: context.read().handleOnCameraIdle, myLocationEnabled: false, - inputController: - context.read().hmsController, + inputController: context.read().hmsController, showCenterMarker: true, ), Align( alignment: AlignmentDirectional.topStart, - child: Utils.buildSvgWithAssets( - icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h) - .onPress(() { - context - .read() - .flushPickupInformation(); + child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h).onPress(() { + context.read().flushPickupInformation(); Navigator.pop(context); }), ).paddingOnly(top: 51.h, left: 24.h), @@ -119,8 +91,7 @@ class CallAmbulancePage extends StatelessWidget { Widget FixedBottomSheet(BuildContext context) { return GestureDetector( - onVerticalDragUpdate: (details){ - + onVerticalDragUpdate: (details) { // if(details.delta.dy<0){ // // context.read().updateBottomSheetState(BottomSheetType.EXPANDED); @@ -161,13 +132,13 @@ class CallAmbulancePage extends StatelessWidget { // ), // ), // ), - // .paddingOnly(right: 24.h, bottom: 24.h), + // .paddingOnly(right: 24.h, bottom: 24.h), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, spacing: 24.h, children: [ - // inputFields(context), + inputFields(context), SizedBox( height: 200.h, child: DecoratedBox( @@ -189,9 +160,7 @@ class CallAmbulancePage extends StatelessWidget { weight: FontWeight.w600, color: AppColors.textColor, ), - " Please select the details of pickup" - .needTranslation - .toText12( + " Please select the details of pickup".needTranslation.toText12( fontWeight: FontWeight.w500, color: AppColors.greyTextColor, ) @@ -200,9 +169,7 @@ class CallAmbulancePage extends StatelessWidget { CustomButton( text: "Select Details".needTranslation, onPressed: () { - context - .read() - .updateBottomSheetState(BottomSheetType.EXPANDED); + context.read().updateBottomSheetState(BottomSheetType.EXPANDED); }) ], ).paddingOnly(top: 24.h, bottom: 32.h, left: 24.h, right: 24.h), @@ -217,11 +184,11 @@ class CallAmbulancePage extends StatelessWidget { Widget ExpanedBottomSheet(BuildContext context) { return GestureDetector( - onVerticalDragUpdate: (details){ - if(details.delta.dy>0){ - context.read().updateBottomSheetState(BottomSheetType.FIXED); - } - }, + onVerticalDragUpdate: (details) { + if (details.delta.dy > 0) { + context.read().updateBottomSheetState(BottomSheetType.FIXED); + } + }, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -239,22 +206,19 @@ class CallAmbulancePage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 16.h, children: [ - hospitalAndPickUpSection(context), - if(context.read().appointment != null) - AppointmentDoctorCard( - renderWidgetForERDisplay: true, - patientAppointmentHistoryResponseModel: context.read().appointment!, - onAskDoctorTap: () {}, - onCancelTap: () async {}, - onRescheduleTap: () async {}, - ).onPress((){ - openAppointmentList(context); - }) - + if (context.read().appointment != null) + AppointmentDoctorCard( + renderWidgetForERDisplay: true, + patientAppointmentHistoryResponseModel: context.read().appointment!, + onAskDoctorTap: () {}, + onCancelTap: () async {}, + onRescheduleTap: () async {}, + ).onPress(() { + openAppointmentList(context); + }) ], - ).paddingOnly(top: 24.h, bottom: 32.h,left: 24.h, right: 24.h), - + ).paddingOnly(top: 24.h, bottom: 32.h, left: 24.h, right: 24.h), bottomPriceContent(context) ], ), @@ -286,8 +250,7 @@ class CallAmbulancePage extends StatelessWidget { shrinkWrap: true, itemCount: 3, itemBuilder: (__, index) { - if (index == - 2) // todo means the end of the list so handle as per the viewmodel + if (index == 2) // todo means the end of the list so handle as per the viewmodel { return CustomButton( height: 40.h, @@ -301,11 +264,8 @@ class CallAmbulancePage extends StatelessWidget { } else { return AddressItem( isSelected: index == 0, - address: - "Flat No 301, Building No 12, Palm Spring Apartment, Sector 45, Gurugram, Haryana 122003", - title: index == 0 - ? "Home".needTranslation - : "Work".needTranslation, + address: "Flat No 301, Building No 12, Palm Spring Apartment, Sector 45, Gurugram, Haryana 122003", + title: index == 0 ? "Home".needTranslation : "Work".needTranslation, onTap: () {}, ); } @@ -354,13 +314,9 @@ class CallAmbulancePage extends StatelessWidget { leadingIcon: AppAssets.pickup_bed, ), CustomSwitch( - value: context - .watch() - .pickupFromInsideTheLocation, - onChanged: (value){ - context - .read() - .updateThePickupPlaceFromLocation(value); + value: context.watch().pickupFromInsideTheLocation, + onChanged: (value) { + context.read().updateThePickupPlaceFromLocation(value); }, ) ], @@ -369,20 +325,17 @@ class CallAmbulancePage extends StatelessWidget { Row( children: [ hospitalAndPickUpItemContent( - title: '', + title: 'Appointment', subTitle: "Have any appointment".needTranslation, - leadingIcon: AppAssets.appointment_checkin_icon, + leadingIcon: AppAssets.appointment_calendar_icon, ), CustomSwitch( - value: context - .watch() - .haveAnAppointment, + value: context.watch().haveAnAppointment, onChanged: (value) async { // if (value) { // openAppointmentList(context); // } - await context.read() - .updateAppointment(value); + await context.read().updateAppointment(value); if (context.read().appointments?.isNotEmpty == true) { openAppointmentList(context); } @@ -405,7 +358,7 @@ class CallAmbulancePage extends StatelessWidget { borderRadius: 12.h, color: AppColors.greyColor, ), - child: Utils.buildSvgWithAssets(icon: leadingIcon), + child: Utils.buildSvgWithAssets(icon: leadingIcon, iconColor: AppColors.greyTextColor), ); } @@ -454,55 +407,47 @@ class CallAmbulancePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ - DecoratedBox( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.scaffoldBgColor, - customBorder: BorderRadius.only( - topLeft: Radius.circular(24.h), - topRight: Radius.circular(24.h), - ), - hasShadow: true + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12.h, + hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12.h, + children: [ + Row( children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4.h, - children: [ - "Total amount to pay".needTranslation.toText18( + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.h, + children: [ + "Total amount to pay".needTranslation.toText18( weight: FontWeight.w600, color: AppColors.textColor, ), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.warning, - height: 18.h, width: 18.h), - SizedBox(width: 4.h,), - "Amount will be paid at the hospital" - .needTranslation - .toText12( + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.warning, height: 18.h, width: 18.h), + SizedBox( + width: 4.h, + ), + "Amount will be paid at the hospital".needTranslation.toText12( fontWeight: FontWeight.w500, color: AppColors.greyTextColor, ), - ], - ) ], - ), - ), + ) + ], + ), + ), Utils.getPaymentAmountWithSymbol( - (Utils.formatNumberToInternationalFormat(context - .read() - .getTotalPrice() ?? - 0)) - .toText24( - fontWeight: FontWeight.w600, - color: AppColors.textColor, - letterSpacing: -2), + (Utils.formatNumberToInternationalFormat(context.read().getTotalPrice() ?? 0)) + .toText24(fontWeight: FontWeight.w600, color: AppColors.textColor, letterSpacing: -2), AppColors.blackColor, 17.h) @@ -518,20 +463,19 @@ class CallAmbulancePage extends StatelessWidget { PlacePrediction? placePrediction = locationViewModel.selectedPrediction; context.read().submitAmbulanceRequest(response?.results.first, placeDetails, placePrediction); }) - ], - ).paddingOnly(top: 24.h, bottom: 12.h, left: 24.h, right: 24.h), - ), + ], + ).paddingOnly(top: 24.h, bottom: 12.h, left: 24.h, right: 24.h), + ), ], ); } - showHospitalBottomSheet(BuildContext context){ + showHospitalBottomSheet(BuildContext context) { showCommonBottomSheetWithoutHeight( - title: - LocaleKeys.selectHospital.tr(), + title: LocaleKeys.selectHospital.tr(), context, child: Consumer( - builder:(_,vm,__)=> HospitalBottomSheetBody( + builder: (_, vm, __) => HospitalBottomSheetBody( searchText: vm.searchController, displayList: vm.displayList, onFacilityClicked: (value) { @@ -545,8 +489,7 @@ class CallAmbulancePage extends StatelessWidget { onHospitalSearch: (value) { vm.searchHospitals(value ?? ""); }, - selectedFacility: - vm.selectedFacility, + selectedFacility: vm.selectedFacility, hmcCount: vm.hmcCount, hmgCount: vm.hmgCount, ), @@ -583,16 +526,14 @@ class CallAmbulancePage extends StatelessWidget { textPlaceInput(context) { return Consumer(builder: (_, vm, __) { - print( - "the data is ${vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description}"); + print("the data is ${vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description}"); return SizedBox( width: MediaQuery.sizeOf(context).width, child: TextInputWidget( labelText: "Enter Pickup Location Manually".needTranslation, hintText: "Enter Pickup Location".needTranslation, controller: TextEditingController( - text: vm.geocodeResponse?.results.first.formattedAddress ?? - vm.selectedPrediction?.description, + text: vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description, ), leadingIcon: AppAssets.location_pickup, isAllowLeadingIcon: true, @@ -614,8 +555,7 @@ class CallAmbulancePage extends StatelessWidget { ///decide which field to show first based on the selected calling place inputFields(BuildContext context) { - return context.read().callingPlace == - AmbulanceCallingPlace.FROM_HOSPITAL + return context.read().callingPlace == AmbulanceCallingPlace.FROM_HOSPITAL ? HospitalFieldFirstThanPlaces(context) : PlaceFirstThanHospitalField(context); } @@ -639,21 +579,15 @@ class CallAmbulancePage extends StatelessWidget { hospitalField(BuildContext context) { return DecoratedBox( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, borderRadius: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), child: Row( children: [ hospitalAndPickUpItemContent( title: "Select Hospital".needTranslation, - subTitle: context - .read() - .getSelectedHospitalName() ?? - "Select Hospital".needTranslation, + subTitle: context.read().getSelectedHospitalName() ?? "Select Hospital".needTranslation, leadingIcon: AppAssets.hospital, ), - Utils.buildSvgWithAssets( - icon: AppAssets.down_cheveron, width: 24.h, height: 24.h) - .paddingAll(16.h) + Utils.buildSvgWithAssets(icon: AppAssets.down_cheveron, width: 24.h, height: 24.h).paddingAll(16.h) ], ).onPress(() { print("the item is clicked"); diff --git a/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart b/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart index 83c16b6..396e509 100644 --- a/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart +++ b/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart @@ -1,15 +1,10 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/tracking_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart' - show CustomPageRoute; import 'package:lottie/lottie.dart'; class RequestingServicesPage extends StatelessWidget { diff --git a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart index de153a3..e5dc0b9 100644 --- a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart +++ b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart @@ -8,12 +8,13 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.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'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/map/HMSMap.dart'; -import 'package:hmg_patient_app_new/widgets/map/map.dart' show GMSMap; +import 'package:hmg_patient_app_new/widgets/map/gms_map.dart' show GMSMap; import 'package:hmg_patient_app_new/widgets/order_tracking/order_tracking_state.dart'; import 'package:hmg_patient_app_new/widgets/order_tracking/request_tracking.dart'; import 'package:lottie/lottie.dart'; @@ -22,11 +23,13 @@ import 'package:url_launcher/url_launcher.dart' show launchUrl; class TrackingScreen extends StatelessWidget { OrderTrackingState? state ; + final bool isRRTOrder; final AmbulanceRequestOrdersModel? order; + final GetCMCAllOrdersResponseModel? rrtOrder; - TrackingScreen({super.key, OrderTrackingState? state, this.order}){ + TrackingScreen({super.key, OrderTrackingState? state, this.order, this.isRRTOrder = false, this.rrtOrder}){ if(state == null){ - switch (order?.statusId) { + switch (order?.statusId ?? rrtOrder?.statusId) { case 1: //pending case 2: //processing this.state = OrderTrackingState.waitingForCall; @@ -53,7 +56,8 @@ class TrackingScreen extends StatelessWidget { visible: state == OrderTrackingState.ended, child: SafeArea( child: CustomButton( - height: 56.h, + height: 40.h, + iconSize: 18.w, backgroundColor: AppColors.bgGreenColor, borderColor: Colors.transparent, text: "Close".needTranslation, @@ -70,7 +74,7 @@ class TrackingScreen extends StatelessWidget { animationSection(), Column( spacing: 16.h, - children: [orderStatus(context), orderTrackingId(), contactSection()], + children: [orderStatus(context), orderTrackingId(context), contactSection()], ).paddingAll(16.h), ], ))), @@ -140,6 +144,8 @@ class TrackingScreen extends StatelessWidget { borderColor: AppColors.primaryRedColor, textColor: Colors.white, icon: AppAssets.cancel, + height: 40.h, + iconSize: 18.w, ), ], ); @@ -153,9 +159,10 @@ class TrackingScreen extends StatelessWidget { mapSection(context), CustomButton( height: 40.h, + iconSize: 18.w, backgroundColor: AppColors.lightRedButtonColor, borderColor: Colors.transparent, - text: "Share Your Live Locatin on Whatsapp".needTranslation, + text: "Share Your Live Location on Whatsapp".needTranslation, fontSize: 12.f, textColor: AppColors.primaryRedColor, iconColor: AppColors.primaryRedColor, @@ -222,13 +229,13 @@ class TrackingScreen extends StatelessWidget { width: 36.h, child: CustomButton( text: '', - iconSize: 16.h, + iconSize: 18.h, icon: AppAssets.call_fill, onPressed: () {}, backgroundColor: AppColors.lightRedButtonColor, iconColor: AppColors.primaryRedColor, borderColor: Colors.transparent, - height: 36.h, + height: 40.h, ), ) ], @@ -259,11 +266,7 @@ class TrackingScreen extends StatelessWidget { informationRegardingTrackingSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - currentStatus(), - SizedBox(height: 4.h), - "You will receive a call from HMG for confirmation ".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500) - ], + children: [currentStatus(), SizedBox(height: 4.h), "You will receive a call from HMG for confirmation ".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500)], ); } @@ -281,49 +284,50 @@ class TrackingScreen extends StatelessWidget { style: TextStyle( fontSize: 21.f, fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), - TextSpan( - text: "...".needTranslation, - style: TextStyle( + color: AppColors.textColor, + ), + ), + TextSpan( + text: "...".needTranslation, + style: TextStyle( fontSize: 21.f, fontWeight: FontWeight.w600, - color: AppColors.errorColor, - ), - ), - ]), - ); - case OrderTrackingState.returning: - return RichText( - text: TextSpan(children: [ - TextSpan( - text: "15:30".needTranslation, - style: TextStyle( + color: AppColors.errorColor, + ), + ), + ]), + ); + case OrderTrackingState.returning: + return RichText( + text: TextSpan(children: [ + TextSpan( + text: "15:30".needTranslation, + style: TextStyle( fontSize: 21.f, fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), - TextSpan( - text: " mins ".needTranslation, - style: TextStyle( + color: AppColors.textColor, + ), + ), + TextSpan( + text: " mins ".needTranslation, + style: TextStyle( fontSize: 21.f, fontWeight: FontWeight.w600, - color: AppColors.errorColor, - ), - ), - TextSpan( - text: "to hospital".needTranslation, - style: TextStyle( + color: AppColors.errorColor, + ), + ), + TextSpan( + text: "to hospital".needTranslation, + style: TextStyle( fontSize: 21.f, fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), - ]), - ); - case OrderTrackingState.ended: + color: AppColors.textColor, + ), + ), + ]), + ); + case OrderTrackingState.ended: + return "Arrived".needTranslation.toText21(color: AppColors.textColor, weight: FontWeight.w600); case OrderTrackingState.failed: case OrderTrackingState.cancel: @@ -331,11 +335,12 @@ class TrackingScreen extends StatelessWidget { } } - orderTrackingId() { + orderTrackingId(BuildContext context) { if(state == OrderTrackingState.failed){ return SizedBox.shrink(); } return Container( + width: MediaQuery.sizeOf(context).width-32.h, padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -347,20 +352,10 @@ class TrackingScreen extends StatelessWidget { spacing: 8.h, children: [ - Visibility(visible:order != null ,child: "Req ID: ${order?.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600)), - Row( - spacing: 8.h, - children: [ - Flexible(child: - chip(order?.pickupLocation??"", AppAssets.location_pickup, AppColors.blackBgColor), - - ), - Flexible(child: - chip(order?.dropOffLocation??"", AppAssets.hospital, AppColors.blackBgColor), + Visibility(visible:(order != null || rrtOrder != null) ,child: "Req ID: ${order?.iD?? rrtOrder?.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600)), + if(order != null) ambulanceOrderData() + // else rrtOrderData() - ) - ], - ), ], ), ); @@ -376,6 +371,7 @@ class TrackingScreen extends StatelessWidget { } contactSection() { + if(isRRTOrder) return SizedBox.shrink(); return Container( padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -390,8 +386,12 @@ class TrackingScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 4.h, children: [ - "Contact Rapid Response Team (RRT)".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w600), - "0115259555".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + "Contact".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w600), + "0115259555".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500).onPress((){ + launchUrl( + Uri.parse("tel://0115259555"), + ); + }), SizedBox(height: 8.h), ], ), @@ -459,6 +459,44 @@ class TrackingScreen extends StatelessWidget { openCancelOrderBottomSheet(BuildContext context){ - context.read().cancelOrder(order, shouldPop: true); + if(isRRTOrder){ + context.read().cancelRRTOrder(rrtOrder?.iD??-1, shouldPop: true); + return; + }else { + context.read().cancelOrder(order, shouldPop: true); + return; + } } + + Widget ambulanceOrderData() { + return Row( + spacing: 8.h, + children: [ + Flexible(child: + chip(order?.pickupLocation??"", AppAssets.location_pickup, AppColors.blackBgColor), + + ), + Flexible(child: + chip(order?.dropOffLocation??"", AppAssets.hospital, AppColors.blackBgColor), + + ) + ], + ); + } + + // Widget rrtOrderData() { + // return Row( + // spacing: 8.h, + // children: [ + // Flexible(child: + // chip(rrtOrder?.pickupLocation??"", AppAssets.location_pickup, AppColors.blackBgColor), + // + // ), + // // Flexible(child: + // // chip(rrtOrder?.??"", AppAssets.hospital, AppColors.blackBgColor), + // // + // // ) + // ], + // ); + // } } diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart b/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart index b1e740b..7d24393 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart @@ -5,22 +5,13 @@ import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/debouncer.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:provider/provider.dart'; class HospitalBottomSheetBody extends StatelessWidget { @@ -82,16 +73,16 @@ class HospitalBottomSheetBody extends StatelessWidget { { var hospital = displayList?[index]; return HospitalListItem( - hospitalData: Utils.convertToPatientDoctorAppointmentList(hospital), - isLocationEnabled: true, + hospitalData: Utils.convertToPatientDoctorAppointmentList(hospital), + isLocationEnabled: true, ).onPress(() { onHospitalClicked(hospital!); });}, separatorBuilder: (_, __) => SizedBox( - height: 16.h, - ), + height: 16.h, + ), itemCount: displayList?.length ?? 0, - )) + )) ], ); } diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart index de20d1a..be1d380 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart @@ -2,12 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart' show AppAssets; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/PatientERTransportationMethod.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart' show TransportOptionItem; -import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:provider/provider.dart'; class AmbulanceOptionSelectionBottomSheet extends StatelessWidget { diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart b/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart index 6f5db92..e1f7420 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart @@ -1,13 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:provider/provider.dart'; class AppointmentBottomSheet extends StatelessWidget{ diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart b/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart index 5d073d4..051c850 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart @@ -23,138 +23,158 @@ class PickupLocation extends StatelessWidget { children: [ "Select Pickup Direction" .needTranslation - .toText24(color: AppColors.textColor,isBold: true), + .toText24(color: AppColors.textColor, isBold: true), SizedBox( height: 16.h, ), "Select Direction" .needTranslation - .toText14(color: AppColors.textColor, weight: FontWeight.w600), + .toText16(color: AppColors.textColor, weight: FontWeight.w600), SizedBox( height: 12.h, ), Selector( - selector: (context, viewModel) => viewModel.callingPlace, - builder: (context, value, _) { - return Column( - spacing: 12.h, - children: [ - RadioGroup( - groupValue: value, - onChanged: (value) { - context - .read() - .updateCallingPlace(value); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - spacing: 24.h, - children: [ - Row( - children: [ - Radio( - value: AmbulanceCallingPlace.TO_HOSPITAL, - groupValue: value, - activeColor: AppColors.primaryRedColor, - - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - "To Hospital" - .needTranslation - .toText12(color: AppColors.textColor) - ], - ).onPress((){ - context - .read() - .updateCallingPlace(AmbulanceCallingPlace.TO_HOSPITAL); - }), - Row( - children: [ - Radio( - value: AmbulanceCallingPlace.FROM_HOSPITAL, - activeColor: AppColors.primaryRedColor, - - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - "From Hospital" - .needTranslation - .toText12(color: AppColors.textColor) - ], - ).onPress((){ - context - .read() - .updateCallingPlace(AmbulanceCallingPlace.FROM_HOSPITAL); - }), - ], + selector: (context, viewModel) => viewModel.callingPlace, + builder: (context, value, _) { + return Column( + children: [ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Radio( + value: AmbulanceCallingPlace.TO_HOSPITAL, + groupValue: value, + onChanged: (AmbulanceCallingPlace? newValue) { + if (newValue != null) { + context + .read() + .updateCallingPlace(newValue); + } + }, + activeColor: AppColors.primaryRedColor, + fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + ), + "To Hospital" + .needTranslation + .toText14(color: AppColors.textColor, weight: FontWeight.w500) + ], + ).onPress(() { + context + .read() + .updateCallingPlace(AmbulanceCallingPlace.TO_HOSPITAL); + }), ), - ), - Visibility( - visible: value == AmbulanceCallingPlace.TO_HOSPITAL, - child: Selector( - selector: (context, viewModel) => - viewModel.ambulanceDirection, - builder: (context, directionValue, _) { - return Column( - spacing: 12.h, + Expanded( + child: Row( + children: [ + Radio( + value: AmbulanceCallingPlace.FROM_HOSPITAL, + groupValue: value, + onChanged: (AmbulanceCallingPlace? newValue) { + if (newValue != null) { + context + .read() + .updateCallingPlace(newValue); + } + }, + activeColor: AppColors.primaryRedColor, + fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + ), + "From Hospital" + .needTranslation + .toText14(color: AppColors.textColor, weight: FontWeight.w500) + ], + ).onPress(() { + context + .read() + .updateCallingPlace(AmbulanceCallingPlace.FROM_HOSPITAL); + }), + ), + ], + ), + Visibility( + visible: value == AmbulanceCallingPlace.TO_HOSPITAL, + child: Selector( + selector: (context, viewModel) => viewModel.ambulanceDirection, + builder: (context, directionValue, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + "Select Way" + .needTranslation + .toText16(color: AppColors.textColor, weight: FontWeight.w600), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - "Select Way" - .needTranslation - .toText14(color: AppColors.textColor, weight: FontWeight.w600), - RadioGroup( - groupValue: directionValue, - onChanged: (value) { + Expanded( + child: Row( + children: [ + Radio( + value: AmbulanceDirection.ONE_WAY, + groupValue: directionValue, + onChanged: (AmbulanceDirection? newValue) { + if (newValue != null) { + context + .read() + .updateDirection(newValue); + } + }, + activeColor: AppColors.primaryRedColor, + fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + ), + "One Way" + .needTranslation + .toText12(color: AppColors.textColor, fontWeight: FontWeight.w500) + ], + ).onPress(() { context .read() - .updateDirection(value); - }, + .updateDirection(AmbulanceDirection.ONE_WAY); + }), + ), + Expanded( child: Row( - mainAxisAlignment: MainAxisAlignment.start, - spacing: 24.h, children: [ - Row( - children: [ - Radio( - value: AmbulanceDirection.ONE_WAY, - activeColor: AppColors.primaryRedColor, - - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - "One Way" - .needTranslation - .toText12(color: AppColors.textColor) - ], - ).onPress((){ - context - .read() - .updateDirection(AmbulanceDirection.ONE_WAY); - }), - Row( - children: [ - Radio( - value: AmbulanceDirection.TWO_WAY, - // activeColor: AppColors.primaryRedColor, - - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - "Two Way" - .needTranslation - .toText12(color: AppColors.textColor) - ], - ).onPress((){ - context - .read() - .updateDirection(AmbulanceDirection.TWO_WAY); - }), + Radio( + value: AmbulanceDirection.TWO_WAY, + groupValue: directionValue, + onChanged: (AmbulanceDirection? newValue) { + if (newValue != null) { + context + .read() + .updateDirection(newValue); + } + }, + activeColor: AppColors.primaryRedColor, + fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + ), + "Two Way" + .needTranslation + .toText14(color: AppColors.textColor, weight: FontWeight.w500) ], - ), + ).onPress(() { + context + .read() + .updateDirection(AmbulanceDirection.TWO_WAY); + }), ), ], - ); - }), - ) - ], - ); - }), + ), + ], + ); + }, + ), + ) + ], + ); + }, + ), SizedBox( height: 16.h, ), @@ -169,4 +189,4 @@ class PickupLocation extends StatelessWidget { ], ); } -} +} \ No newline at end of file diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart index d2e9b21..1b85165 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart @@ -2,12 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:provider/provider.dart' show Consumer; class TypeSelectionWidget extends StatelessWidget { final String hmcCount; @@ -44,7 +41,7 @@ class TypeSelectionWidget extends StatelessWidget { ?AppColors.secondaryLightRedColor: AppColors.whiteColor, textColor: selectedFacility == FacilitySelection.ALL ? AppColors.errorColor:AppColors.blackColor - , + , ).onPress((){ onitemClicked(FacilitySelection.ALL); }), diff --git a/lib/presentation/emergency_services/emergency_services_page.dart b/lib/presentation/emergency_services/emergency_services_page.dart index 5d7c11b..3833d32 100644 --- a/lib/presentation/emergency_services/emergency_services_page.dart +++ b/lib/presentation/emergency_services/emergency_services_page.dart @@ -1,25 +1,23 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_request_type_select.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/call_ambulance_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/history/er_history_listing.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/nearest_er_page.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'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; @@ -28,16 +26,17 @@ class EmergencyServicesPage extends StatelessWidget { EmergencyServicesPage({super.key}); late EmergencyServicesViewModel emergencyServicesViewModel; - LocationUtils? locationUtils; + + _handleConfirmationBottomSheet() {} @override Widget build(BuildContext context) { emergencyServicesViewModel = Provider.of(context, listen: false); - locationUtils = getIt.get(); - locationUtils!.isShowConfirmDialog = true; + return CollapsingListView( - title: "Emergency Services".needTranslation, + title: LocaleKeys.emergencyServices.tr(), requests: () { + emergencyServicesViewModel.changeOrderDisplayItems(OrderDislpay.ALL); Navigator.of(context).push(CustomPageRoute(page: ErHistoryListing(), direction: AxisDirection.up)); }, child: Padding( @@ -61,7 +60,9 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Call Ambulance".needTranslation.toText16(isBold: true, color: AppColors.blackColor), - "Request and ambulance in emergency from home or hospital".needTranslation.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "Request an ambulance in emergency from home or hospital" + .needTranslation + .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), @@ -69,86 +70,66 @@ class EmergencyServicesPage extends StatelessWidget { Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), ], ).onPress(() { - - locationUtils!.getLocation( - isShowConfirmDialog: true, - onSuccess: (position) { - // updateBottomSheetState(BottomSheetType.FIXED); - Navigator.of(context).push( - CustomPageRoute( - page: CallAmbulancePage(), direction: AxisDirection.down), - ); - }); - // showCommonBottomSheetWithoutHeight( - // context, - // child: Container( - // decoration: - // RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.primaryRedColor, - // borderRadius: 24.h, - // ), - // child: Padding( - // padding: EdgeInsets.all(24.h), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // "".toText14(), - // Utils.buildSvgWithAssets( - // icon: AppAssets.cancel_circle_icon, - // iconColor: AppColors.whiteColor, - // width: 24.h, - // height: 24.h, - // fit: BoxFit.contain, - // ).onPress(() { - // Navigator.of(context).pop(); - // }), - // ], - // ), - // Lottie.asset(AppAnimations.ambulance_alert, - // repeat: false, - // reverse: false, - // frameRate: FrameRate(60), - // width: 120.h, - // height: 120.h, - // fit: BoxFit.contain), - // SizedBox(height: 8.h), - // "Confirmation".needTranslation.toText28( - // color: AppColors.whiteColor, isBold: true), - // SizedBox(height: 8.h), - // "Are you sure you want to call an ambulance?" - // .needTranslation - // .toText14( - // color: AppColors.whiteColor, - // weight: FontWeight.w500), - // SizedBox(height: 24.h), - // CustomButton( - // text: LocaleKeys.confirm.tr(context: context), - // onPressed: () async { - // // - // Navigator.of(context).pop(); - // await emergencyServicesViewModel.getTransportationOption(); - // openTranportationSelectionBottomSheet(context); - // }, - // backgroundColor: AppColors.whiteColor, - // borderColor: AppColors.whiteColor, - // textColor: AppColors.primaryRedColor, - // icon: AppAssets.checkmark_icon, - // iconColor: AppColors.primaryRedColor, - // ), - // SizedBox(height: 8.h), - // ], - // ), - // ), - // ), - // isFullScreen: false, - // isCloseButtonVisible: false, - // hasBottomPadding: false, - // backgroundColor: AppColors.primaryRedColor, - // callBackFunc: () {}, - // ); + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: AppColors.whiteColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + "Confirmation".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), + SizedBox(height: 8.h), + "Are you sure you want to call an ambulance?" + .needTranslation + .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).pop(); + await emergencyServicesViewModel.getTransportationOption(); + openTranportationSelectionBottomSheet(context); + }, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.whiteColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), + ), + ), + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, + ); }), ), SizedBox(height: 16.h), @@ -168,7 +149,9 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Nearest ER Location".needTranslation.toText16(isBold: true, color: AppColors.blackColor), - "Get the details of nearest branch including directions".needTranslation.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "Get the details of nearest branch including directions" + .needTranslation + .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), @@ -196,7 +179,8 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Rapid Response Team (RRT)".toText16(isBold: true, color: AppColors.blackColor), - "Comprehensive medical service for all sorts of urgent and stable cases".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "Comprehensive medical service for all sorts of urgent and stable cases" + .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), @@ -211,6 +195,104 @@ class EmergencyServicesPage extends StatelessWidget { color: AppColors.primaryRedColor, borderRadius: 24.h, ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), + SizedBox(height: 8.h), + "Are you sure you want to call Rapid Response Team (RRT)?" + .needTranslation + .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).pop(); + + LoaderBottomSheet.showLoader(); + emergencyServicesViewModel.clearRRTData(); + await emergencyServicesViewModel.getRRTProcedures(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + padding: EdgeInsets.only(top: 24.h), + titleWidget: Transform.flip( + flipX: emergencyServicesViewModel.isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + iconColor: Color(0xff2B353E), + fit: BoxFit.contain, + ), + ).onPress(() { + + Navigator.pop(context); + }), + // title: "Rapid Response Team (RRT)".needTranslation, + context, + child: RrtRequestTypeSelect(), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + }); + }, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.whiteColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), + ), + ), + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () { + context.read().setTermsAndConditions(false); + }, + ); + }), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rrt_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Emergency Check-In".needTranslation.toText16(isBold: true, color: AppColors.blackColor), + "Prior ER Check-In to skip the line & payment at the reception." + .needTranslation + .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + ], + ), + ), + SizedBox(width: 12.h), + Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.primaryRedColor, borderRadius: 24.h), child: Padding( padding: EdgeInsets.all(24.h), child: Column( @@ -231,26 +313,22 @@ class EmergencyServicesPage extends StatelessWidget { }), ], ), - Lottie.asset(AppAnimations.ambulance_alert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + Lottie.asset(AppAnimations.ambulanceAlert, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), - "Confirmation".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), + LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), SizedBox(height: 8.h), - "Are you sure you want to call Rapid Response Team (RRT)?".needTranslation.toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + "Are you sure you want to make ER Check-In?".needTranslation.toText14(color: AppColors.whiteColor, weight: FontWeight.w500), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.confirm.tr(context: context), onPressed: () async { Navigator.of(context).pop(); - showCommonBottomSheetWithoutHeight( - title: "Rapid Response Team (RRT)".needTranslation, - context, - child: RrtRequestTypeSelect(), - isFullScreen: false, - isCloseButtonVisible: true, - hasBottomPadding: false, - backgroundColor: AppColors.bottomSheetBgColor, - callBackFunc: () {}, - ); + LoaderBottomSheet.showLoader(loadingText: "Checking your ER Appointment status...".needTranslation); + await context.read().checkPatientERAdvanceBalance(onSuccess: (dynamic response) { + LoaderBottomSheet.hideLoader(); + context.read().navigateToEROnlineCheckIn(); + }); }, backgroundColor: AppColors.whiteColor, borderColor: AppColors.whiteColor, @@ -277,60 +355,51 @@ class EmergencyServicesPage extends StatelessWidget { ); } - openPickupDetailsBottomSheet(BuildContext context){ + openPickupDetailsBottomSheet(BuildContext context) { showCommonBottomSheetWithoutHeight( - onCloseClicked: (){ - context - .read() - .flushPickupInformation(); + onCloseClicked: () { + context.read().flushPickupInformation(); }, titleWidget: Transform.flip( - flipX: emergencyServicesViewModel.isArabic ? true : false, + flipX: emergencyServicesViewModel.isArabic, child: Utils.buildSvgWithAssets( icon: AppAssets.arrow_back, iconColor: Color(0xff2B353E), fit: BoxFit.contain, ), ).onPress(() { - context - .read() - .flushPickupInformation(); + context.read().flushPickupInformation(); Navigator.pop(context); openTranportationSelectionBottomSheet(context); }), context, - child: PickupLocation( - onTap: () { - Navigator.of(context).pop(); - context.read().flushSearchPredictions(); - context - .read() - .navigateTOAmbulancePage(); - }), + child: PickupLocation(onTap: () { + Navigator.of(context).pop(); + context.read().flushSearchPredictions(); + context.read().navigateTOAmbulancePage(); + }), isFullScreen: false, isCloseButtonVisible: true, hasBottomPadding: false, - backgroundColor: AppColors.bottomSheetBgColor, callBackFunc: () {}, ); } void openTranportationSelectionBottomSheet(BuildContext context) { - if(emergencyServicesViewModel.transportationOptions.isNotEmpty) { + if (emergencyServicesViewModel.transportationOptions.isNotEmpty) { showCommonBottomSheetWithoutHeight( title: "Transport Options".needTranslation, context, child: SizedBox( height: 400.h, - child: AmbulanceOptionSelectionBottomSheet( - onTap: () { - Navigator.of(context).pop(); - openPickupDetailsBottomSheet(context); - // context - // .read() - // .navigateTOAmbulancePage(); - }), + child: AmbulanceOptionSelectionBottomSheet(onTap: () { + Navigator.of(context).pop(); + openPickupDetailsBottomSheet(context); + // context + // .read() + // .navigateTOAmbulancePage(); + }), ), isFullScreen: false, isCloseButtonVisible: true, diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart new file mode 100644 index 0000000..ee061b7 --- /dev/null +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart @@ -0,0 +1,180 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +import '../call_ambulance/widgets/HospitalBottomSheetBody.dart'; + +class ErOnlineCheckinHome extends StatelessWidget { + ErOnlineCheckinHome({super.key}); + + late EmergencyServicesViewModel emergencyServicesViewModel; + bool _supportsNFC = false; + + @override + Widget build(BuildContext context) { + emergencyServicesViewModel = Provider.of(context, listen: false); + FlutterNfcKit.nfcAvailability.then((value) { + _supportsNFC = (value == NFCAvailability.available); + }); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Emergency Check-In".needTranslation, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.h, height: 58.h), + SizedBox(width: 18.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Online Check-In".needTranslation.toText18(color: AppColors.textColor, isBold: true), + "This service lets patients to register their ER appointment prior to arrival.".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + CustomButton( + text: emergencyServicesViewModel.patientHasAdvanceERBalance ? LocaleKeys.checkinOption.tr() : LocaleKeys.bookAppo.tr(), + onPressed: () async { + if (emergencyServicesViewModel.patientHasAdvanceERBalance) { + Future.delayed(const Duration(milliseconds: 500), () { + showNfcReader(context, onNcfScan: (String nfcId) { + Future.delayed(const Duration(milliseconds: 100), () async { + print(nfcId); + LoaderBottomSheet.showLoader(loadingText: "Processing check-in...".needTranslation); + await emergencyServicesViewModel.getProjectIDFromNFC( + nfcCode: nfcId, + onSuccess: (value) async { + await emergencyServicesViewModel.autoGenerateInvoiceERClinic( + projectID: value, + onSuccess: (value) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.onlineCheckIn.tr(), + child: Utils.getSuccessWidget(loadingText: "Your ER Online Check-In has been successfully done. Please proceed to the waiting area.".needTranslation), + callBackFunc: () { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, isFullScreen: false); + }, + onError: (errMessage) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unexpected error occurred during check-in. Please contact support.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + onError: (err) {}); + }); + }, onCancel: () {}); + }); + // showCommonBottomSheetWithoutHeight(context, + // title: LocaleKeys.onlineCheckIn.tr(), + // child: ErOnlineCheckinSelectCheckinBottomSheet( + // projectID: 15, + // ), + // callBackFunc: () {}, + // isFullScreen: false); + } else { + LoaderBottomSheet.showLoader(loadingText: "Fetching hospitals list...".needTranslation); + await context.read().getProjects(); + LoaderBottomSheet.hideLoader(); + //Project Selection Dropdown + showHospitalBottomSheet(context); + } + }, + backgroundColor: emergencyServicesViewModel.patientHasAdvanceERBalance ? AppColors.alertColor : AppColors.primaryRedColor, + borderColor: emergencyServicesViewModel.patientHasAdvanceERBalance ? AppColors.alertColor : AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: emergencyServicesViewModel.patientHasAdvanceERBalance ? AppAssets.appointment_checkin_icon : AppAssets.bookAppoBottom, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ); + } + + showHospitalBottomSheet(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.selectHospital.tr(), + context, + child: Consumer( + builder: (_, vm, __) => HospitalBottomSheetBody( + displayList: vm.displayList, + onFacilityClicked: (value) { + vm.setSelectedFacility(value); + vm.getDisplayList(); + }, + onHospitalClicked: (hospital) async { + Navigator.pop(context); + vm.setSelectedHospital(hospital); + LoaderBottomSheet.showLoader(loadingText: "Fetching payment information...".needTranslation); + await vm.getPatientERPaymentInformation(onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + vm.navigateToEROnlineCheckInPaymentPage(); + }); + }, + onHospitalSearch: (value) { + vm.searchHospitals(value ?? ""); + }, + selectedFacility: vm.selectedFacility, + hmcCount: vm.hmcCount, + hmgCount: vm.hmgCount, + searchText: vm.searchController, + ), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + } +} diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart new file mode 100644 index 0000000..f48efe7 --- /dev/null +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart @@ -0,0 +1,176 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class ErOnlineCheckinPaymentDetailsPage extends StatelessWidget { + ErOnlineCheckinPaymentDetailsPage({super.key}); + + late AppState appState; + late EmergencyServicesViewModel emergencyServicesViewModel; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + emergencyServicesViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Emergency Check-In".needTranslation, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "ER Visit Details".needTranslation.toText18(color: AppColors.textColor, isBold: true), + SizedBox(height: 24.h), + Row( + children: [ + "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}".toText14(color: AppColors.textColor, isBold: true), + ], + ), + SizedBox(height: 12.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "File No.: ${appState.getAuthenticatedUser()!.patientId!.toString()}", + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: "ER Clinic".needTranslation, + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: emergencyServicesViewModel.selectedHospital!.name, + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + icon: AppAssets.calendar, + labelText: DateUtil.formatDateToDate(DateTime.now(), false), + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + ], + ), + SizedBox(height: 12.h), + ], + ), + ), + ) + ], + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText18(isBold: true), + Utils.getPaymentAmountWithSymbol(emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShare.toString().toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: true), + ], + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: "".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor)), + "VAT 15% (${emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientTaxAmount})" + .needTranslation + .toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -1), + ], + ), + SizedBox(height: 18.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150.h, + child: Utils.getPaymentMethods(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Utils.getPaymentAmountWithSymbol( + emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax.toString().toText24(isBold: true), AppColors.blackColor, 17, + isSaudiCurrency: true), + ], + ), + ], + ) + ], + ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), + CustomButton( + text: LocaleKeys.payNow.tr(), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute(page: ErOnlineCheckinPaymentPage()), + ); + }, + backgroundColor: AppColors.infoColor, + borderColor: AppColors.infoColor.withOpacity(0.01), + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 56.h, + icon: AppAssets.appointment_pay_icon, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(16.h, 24.h), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart new file mode 100644 index 0000000..8de7518 --- /dev/null +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart @@ -0,0 +1,510 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class ErOnlineCheckinPaymentPage extends StatefulWidget { + ErOnlineCheckinPaymentPage({super.key}); + + @override + State createState() => _ErOnlineCheckinPaymentPageState(); +} + +class _ErOnlineCheckinPaymentPageState extends State { + late PayfortViewModel payfortViewModel; + late EmergencyServicesViewModel emergencyServicesViewModel; + + late AppState appState; + + MyInAppBrowser? browser; + + String selectedPaymentMethod = ""; + + String transID = ""; + + bool isShowTamara = false; + + String tamaraPaymentStatus = ""; + + String tamaraOrderID = ""; + + @override + void initState() { + scheduleMicrotask(() { + payfortViewModel.initPayfortViewModel(); + // payfortViewModel.getTamaraInstallmentsDetails().then((val) { + // if (emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax! >= payfortViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + // emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax! <= payfortViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + // setState(() { + // isShowTamara = true; + // }); + // } + // }); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + payfortViewModel = Provider.of(context, listen: false); + emergencyServicesViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Emergency Check-In".needTranslation, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "MADA"; + openPaymentURL("mada"); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), + ], + ), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "VISA"; + openPaymentURL("visa"); + }), + SizedBox(height: 16.h), + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Tamara".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("tamara"); + }) + : SizedBox.shrink(), + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Consumer(builder: (context, payfortVM, child) { + //TODO: Need to add loading state & animation for Apple Pay Configuration + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.isCash ?? true) + ? Container( + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.secondaryLightRedBorderColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Insurance expired or inactive".needTranslation.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500).paddingSymmetrical(24.h, 0.h), + CustomButton( + text: LocaleKeys.updateInsurance.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.secondaryLightRedBorderColor, + textColor: AppColors.whiteColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), + ], + ), + ) + : const SizedBox(), + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShare.toString().toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientTaxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax.toString().toText24(isBold: true), AppColors.blackColor, 17, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Platform.isIOS + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (Utils.havePrivilege(103)) { + startApplePay(); + } else { + openPaymentURL("ApplePay"); + } + }) + : SizedBox(height: 12.h), + SizedBox(height: 12.h), + ], + ); + }), + ), + ], + ), + ); + } + + openPaymentURL(String paymentMethod) { + browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context); + transID = Utils.getAdvancePaymentTransID( + emergencyServicesViewModel.selectedHospital!.iD, + appState.getAuthenticatedUser()!.patientId!, + ); + + //TODO: Need to pass dynamic params to the payment request instead of static values + browser?.openPaymentBrowser( + emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, + "ER Online Check-In Payment", + transID, + emergencyServicesViewModel.selectedHospital!.iD.toString(), + "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + selectedPaymentMethod, + appState.getAuthenticatedUser()!.patientType.toString(), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!, + browser!, + false, + "3", + "", + context, + null, + 0, + 10, + 0, + "3"); + } + + startApplePay() async { + // showCommonBottomSheet(context, + // child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false); + LoaderBottomSheet.showLoader(); + transID = Utils.getAdvancePaymentTransID( + emergencyServicesViewModel.selectedHospital!.iD, + appState.getAuthenticatedUser()!.patientId!, + ); + + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); + + await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), projectId: emergencyServicesViewModel.selectedHospital!.iD); + + applePayInsertRequest.clientRequestID = transID; + applePayInsertRequest.clinicID = 10; + + applePayInsertRequest.currency = appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com"; + applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + + applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken); + applePayInsertRequest.doctorID = 0; + applePayInsertRequest.projectID = emergencyServicesViewModel.selectedHospital!.iD.toString(); + applePayInsertRequest.serviceID = ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum().toString(); + applePayInsertRequest.channelID = 3; + applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType; + applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa; + applePayInsertRequest.appointmentDate = null; + applePayInsertRequest.appointmentNo = 0; + applePayInsertRequest.orderDescription = "ER Online Check-In Payment"; + applePayInsertRequest.liveServiceID = "0"; + applePayInsertRequest.latitude = "0.0"; + applePayInsertRequest.longitude = "0.0"; + applePayInsertRequest.amount = emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString(); + applePayInsertRequest.isSchedule = "0"; + applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en'; + applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2; + applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId; + applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.paymentOption = "ApplePay"; + + applePayInsertRequest.isMobSDK = true; + applePayInsertRequest.merchantReference = transID; + applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.commandType = "PURCHASE"; + applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.returnURL = ""; + + //TODO: Need to pass dynamic params to the Apple Pay instead of static values + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) { + payfortViewModel.paymentWithApplePay( + customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + // customerEmail: projectViewModel.authenticatedUserObject.user.emailAddress, + customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + orderDescription: "Appointment Payment", + orderAmount: double.parse(emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString()), + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier, + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode, + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest, + currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + log("failureResult: ${failureResult.message.toString()}"); + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + log("successResult: ${successResult.responseMessage.toString()}"); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + checkPaymentStatus(); + }, + ); + }); + } + + void checkPaymentStatus() async { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + await payfortViewModel.checkPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + print(apiResponse.data); + if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { + if (emergencyServicesViewModel.isERBookAppointment) { + await emergencyServicesViewModel.ER_CreateAdvancePayment( + paymentMethodName: selectedPaymentMethod, + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + onSuccess: (value) async { + await emergencyServicesViewModel.addAdvanceNumberRequest( + advanceNumber: value, + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + appointmentNo: "0", + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + if (emergencyServicesViewModel.isERBookAppointment) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Your appointment has been booked successfully. Please perform Check-In once you arrive at the hospital.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else {} + }); + }); + } else {} + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } + + onBrowserLoadStart(String url) { + print("onBrowserLoadStart"); + print(url); + + if (selectedPaymentMethod == "tamara") { + if (Platform.isAndroid) { + Uri uri = new Uri.dataFromString(url); + tamaraPaymentStatus = uri.queryParameters['status']!; + tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!; + } else { + Uri uri = new Uri.dataFromString(url); + tamaraPaymentStatus = uri.queryParameters['paymentStatus']!; + tamaraOrderID = uri.queryParameters['orderId']!; + } + } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.successURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + }); + // } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.errorURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + }); + // } + } + + onBrowserExit(bool isPaymentMade) async { + checkPaymentStatus(); + } +} diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart new file mode 100644 index 0000000..d44686c --- /dev/null +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/common_models/privilege/ProjectDetailListModel.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; + +class ErOnlineCheckinSelectCheckinBottomSheet extends StatelessWidget { + ErOnlineCheckinSelectCheckinBottomSheet({super.key, required this.projectID}); + + bool _supportsNFC = false; + int projectID = 0; + + late LocationUtils locationUtils; + late AppState appState; + ProjectDetailListModel projectDetailListModel = ProjectDetailListModel(); + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + FlutterNfcKit.nfcAvailability.then((value) { + _supportsNFC = (value == NFCAvailability.available); + }); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + checkInOptionCard( + AppAssets.checkin_location_icon, + "Live Location".needTranslation, + "Verify your location to be at hospital to check in".needTranslation, + ).onPress(() { + // locationUtils = LocationUtils( + // isShowConfirmDialog: false, + // navigationService: myAppointmentsViewModel.navigationService, + // appState: myAppointmentsViewModel.appState, + // ); + locationUtils.getCurrentLocation(onSuccess: (value) { + projectDetailListModel = Utils.getProjectDetailObj(appState, projectID); + double dist = Utils.distance(value.latitude, value.longitude, double.parse(projectDetailListModel.latitude!), double.parse(projectDetailListModel.longitude!)).ceilToDouble() * 1000; + print(dist); + if (dist <= projectDetailListModel.geofenceRadius!) { + sendCheckInRequest(projectDetailListModel.checkInQrCode!, context); + } else { + showCommonBottomSheetWithoutHeight(context, + title: "Error".needTranslation, + child: Utils.getErrorWidget(loadingText: "Please ensure you're within the hospital location to perform online check-in.".needTranslation), callBackFunc: () { + Navigator.of(context).pop(); + }, isFullScreen: false); + } + }); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_nfc_icon, + "NFC (Near Field Communication)".needTranslation, + "Scan your phone via NFC board to check in".needTranslation, + ).onPress(() { + Future.delayed(const Duration(milliseconds: 500), () { + showNfcReader(context, onNcfScan: (String nfcId) { + Future.delayed(const Duration(milliseconds: 100), () { + sendCheckInRequest(nfcId, context); + }); + }, onCancel: () {}); + }); + }), + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_qr_icon, + "QR Code".needTranslation, + "Scan QR code with your camera to check in".needTranslation, + ).onPress(() async { + String onlineCheckInQRCode = (await BarcodeScanner.scan().then((value) => value.rawContent)); + if (onlineCheckInQRCode != "") { + sendCheckInRequest(onlineCheckInQRCode, context); + } else {} + }), + ], + ); + } + + Widget checkInOptionCard(String icon, String title, String subTitle) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(isBold: true, color: AppColors.textColor), + subTitle.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ), + ], + ).paddingAll(16.h), + ); + } + + void sendCheckInRequest(String scannedCode, BuildContext context) async { + showCommonBottomSheet(context, + child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false); + // await myAppointmentsViewModel.sendCheckInNfcRequest( + // patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + // scannedCode: scannedCode, + // checkInType: 2, + // onSuccess: (apiResponse) { + // Navigator.of(context).pop(); + // showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () { + // Navigator.of(context).pop(); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); + // Navigator.of(context).push( + // CustomPageRoute(page: MyAppointmentsPage()), + // ); + // }, isFullScreen: false); + // }, + // onError: (error) { + // Navigator.of(context).pop(); + // showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: error), callBackFunc: () { + // Navigator.of(context).pop(); + // }, isFullScreen: false); + // }, + // ); + } +} diff --git a/lib/presentation/emergency_services/history/er_history_listing.dart b/lib/presentation/emergency_services/history/er_history_listing.dart index e5dd1e4..fe1ed31 100644 --- a/lib/presentation/emergency_services/history/er_history_listing.dart +++ b/lib/presentation/emergency_services/history/er_history_listing.dart @@ -1,13 +1,18 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart' show AppAssets; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/ambulance_history_item.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/ambulance_history_item.dart' show AmbulanceHistoryItem; +import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/rrt_item.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/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; class ErHistoryListing extends StatelessWidget { @@ -16,30 +21,53 @@ class ErHistoryListing extends StatelessWidget { return Scaffold( body: Column( children: [ + Expanded( child: CollapsingListView( title: "History Log".needTranslation, child: SingleChildScrollView( physics: NeverScrollableScrollPhysics(), child: Column( - children: [Visibility( - visible: context - .read() - .orders - ?.isNotEmpty == true, - child: ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: context - .read() - .orders - ?.length ?? 0, - itemBuilder: (_, index) => - Consumer( builder: (_, vm, __)=> - AmbulanceHistoryItem(order: vm.orders![index]), - ) + children: [ + + Selector( + selector: (context, vm) => (vm.orderDisplayList, vm.historyLoading), + builder: (context, data, _) { + + return Column( + children: [ + orderChips(context, data.$2, data.$1), + + Visibility( + visible:data.$1.isNotEmpty == true, + child: ListView.builder( + padding: EdgeInsets.only(top:24.h ), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount:data?.$1.length ?? 0, + itemBuilder: (_, index) { + var order = data.$1[index]; + if (order is AmbulanceRequestOrdersModel) { + return AmbulanceHistoryItem(order: order).toShimmer2(isShow: data.$2); + } else { + return RRTItem(order: (order as GetCMCAllOrdersResponseModel)).toShimmer2(isShow: data.$2); + } + }), + ), + Visibility( + visible: data.$1 + ?.isEmpty == true, child: Center( + child: Utils.getNoDataWidget(context, + noDataText: "You don't have any history" + .needTranslation), + )), + ], + ); + } ), - ),] + + + ] ).paddingAll(16.h) )) @@ -47,16 +75,77 @@ class ErHistoryListing extends StatelessWidget { ), - Visibility( - visible: context - .read() - .orders - ?.isEmpty == true, - child: Utils.getNoDataWidget(context, - noDataText: "You don't have any history" - .needTranslation)), + ], ), ); } + + orderChips(BuildContext context, bool isLoading, List dataList) { + + if (dataList?.isEmpty == true) { + return SizedBox.shrink(); + } + return Selector( + selector: (context, vm) => vm.currentlyDisplayedOrder, + builder: (context, value, __) { + return Row( + spacing: 8.h, + children: [ + if(dataList?.isNotEmpty == true) + AppCustomChipWidget( + labelText: "All Facilities".needTranslation, + shape: RoundedRectangleBorder( + side: BorderSide( + color: value == OrderDislpay.ALL ? AppColors.errorColor : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + backgroundColor: value == OrderDislpay.ALL ? AppColors.secondaryLightRedColor : AppColors.whiteColor, + textColor: value == OrderDislpay.ALL ? AppColors.errorColor : AppColors.blackColor, + ).onPress(() { + context.read().changeOrderDisplayItems(OrderDislpay.ALL); + }).toShimmer2(isShow: isLoading), + if(context + .read() + .ambulanceOrders + ?.isNotEmpty == true) + AppCustomChipWidget( + labelText: "Ambulance".needTranslation, + icon: AppAssets.ambulance, + shape: RoundedRectangleBorder( + side: BorderSide( + color: value == OrderDislpay.AMBULANCE ? AppColors.errorColor : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + backgroundColor: value == OrderDislpay.AMBULANCE ? AppColors.secondaryLightRedColor : AppColors.whiteColor, + textColor: value == OrderDislpay.AMBULANCE ? AppColors.errorColor : AppColors.blackColor, + ).toShimmer2(isShow: isLoading).onPress(() { + context.read().changeOrderDisplayItems(OrderDislpay.AMBULANCE); + }), + if(context + .read() + .ordersRRT + ?.completedOrders + .isNotEmpty == true) + AppCustomChipWidget( + labelText: "Rapid Response Team".needTranslation, + icon: AppAssets.ic_rrt_vehicle, + shape: RoundedRectangleBorder( + side: BorderSide( + color: value == OrderDislpay.RRT ? AppColors.errorColor : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + backgroundColor: value == OrderDislpay.RRT ? AppColors.secondaryLightRedColor : AppColors.whiteColor, + textColor: value == OrderDislpay.RRT ? AppColors.errorColor : AppColors.blackColor, + ).toShimmer2(isShow: isLoading).onPress(() { + context.read().changeOrderDisplayItems(OrderDislpay.RRT); + }), + ], + ); + }); + + } } diff --git a/lib/presentation/emergency_services/history/widget/RequestStatus.dart b/lib/presentation/emergency_services/history/widget/RequestStatus.dart index de95bdc..4f39a49 100644 --- a/lib/presentation/emergency_services/history/widget/RequestStatus.dart +++ b/lib/presentation/emergency_services/history/widget/RequestStatus.dart @@ -54,8 +54,8 @@ class RequestStatus extends StatelessWidget { switch (status) { case 1: //pending case 2: - return AppColors.successColor;//processing case 3: //completed + return AppColors.successColor;//processing case 4: //cancel case 6: case 7: diff --git a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart index 5f4c8b2..f3ec388 100644 --- a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart +++ b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart @@ -60,6 +60,8 @@ class AmbulanceHistoryItem extends StatelessWidget { borderColor: AppColors.primaryRedColor, textColor: Colors.white, icon: AppAssets.cancel, + height: 40.h, + iconSize: 18.w, ), ], ).paddingAll(16.h), @@ -71,7 +73,7 @@ class AmbulanceHistoryItem extends StatelessWidget { labelText: title, icon: iconString, iconColor: iconColor, - iconSize: 14.h, + iconSize: 12.h, ); } diff --git a/lib/presentation/emergency_services/history/widget/rrt_item.dart b/lib/presentation/emergency_services/history/widget/rrt_item.dart new file mode 100644 index 0000000..dfb6e79 --- /dev/null +++ b/lib/presentation/emergency_services/history/widget/rrt_item.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/RequestStatus.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +import '../../../../core/utils/utils.dart'; + + +class RRTItem extends StatelessWidget { + final GetCMCAllOrdersResponseModel order; + + const RRTItem({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + hasShadow: true, + customBorder: BorderRadius.all( + Radius.circular(20.h), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + RequestStatus(status: order.statusId ?? 0), + "Req ID: ${order.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Row( + spacing: 4.w, + children: [ + chip( Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(order.time)), AppAssets.calendar, AppColors.blackBgColor), + chip("Rapid Response Team(RRT)".needTranslation, AppAssets.ic_rrt_vehicle, AppColors.blackBgColor), + ], + ), + SizedBox(height: 4.h), + if (order.statusId == 1 || order.statusId == 2) + CustomButton( + text: "Cancel Request".needTranslation, + onPressed: () async { + openCancelOrderBottomSheet(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + icon: AppAssets.cancel, + iconSize: 20.h, + height: 40.h, + ), + ], + ).paddingAll(16.h), + ).paddingOnly(bottom: 16.h); + } + + chip(String title, String iconString, Color iconColor) { + return AppCustomChipWidget( + labelText: title, + icon: iconString, + iconColor: iconColor, + iconSize: 12.h, + ); + } + + openCancelOrderBottomSheet(BuildContext context) { + context.read().cancelRRTOrder(order.iD); + } +} diff --git a/lib/presentation/emergency_services/nearest_er_page.dart b/lib/presentation/emergency_services/nearest_er_page.dart index 13373f1..16863bf 100644 --- a/lib/presentation/emergency_services/nearest_er_page.dart +++ b/lib/presentation/emergency_services/nearest_er_page.dart @@ -14,7 +14,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart' show Selector, WatchContext, ReadContext; -import '../../core/enums.dart' show SelectionTypeEnum; import '../../core/utils/debouncer.dart' show Debouncer; class NearestErPage extends StatefulWidget { diff --git a/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart b/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart index 559a0a5..c5301ea 100644 --- a/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart +++ b/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart @@ -18,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; import '../../../theme/colors.dart'; -import '../../appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; class LocationInputBottomSheet extends StatelessWidget { final Debouncer debouncer = Debouncer(milliseconds: 500); diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index b414799..1341af7 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -90,7 +90,7 @@ class _WalletPaymentConfirmPageState extends State { Transform.flip( flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, height: 13.h, @@ -120,7 +120,7 @@ class _WalletPaymentConfirmPageState extends State { children: [ Image.asset(AppAssets.visa, width: 40.h, height: 40.h), SizedBox(width: 8.h), - Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), ], ).toShimmer2(isShow: false), SizedBox(height: 16.h), @@ -132,7 +132,7 @@ class _WalletPaymentConfirmPageState extends State { Transform.flip( flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, height: 13.h, diff --git a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart index 8e10e28..a46f79d 100644 --- a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart +++ b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart @@ -40,8 +40,8 @@ class HospitalListItemAdvancePayment extends StatelessWidget { child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, - width: 18, - height: 13, + width: 40.h, + height: 40.h, fit: BoxFit.contain, ), ), @@ -67,40 +67,4 @@ class HospitalListItemAdvancePayment extends StatelessWidget { ) ], ); - -// Widget get distanceInfo => Row( -// children: [ -// Visibility( -// visible: (hospitalModel.distanceInKMs != "0"), -// child: AppCustomChipWidget( -// labelText: "${hospitalData?.distanceInKMs ?? ""} km".needTranslation, -// deleteIcon: AppAssets.location_red, -// deleteIconSize: Size(9, 12), -// backgroundColor: AppColors.secondaryLightRedColor, -// textColor: AppColors.errorColor, -// ), -// ), -// Visibility( -// visible: (hospitalData?.distanceInKMs == "0"), -// child: Row( -// children: [ -// AppCustomChipWidget( -// labelText: "Distance not available".needTranslation, -// textColor: AppColors.blackColor, -// ), -// SizedBox( -// width: 8.h, -// ) -// ], -// )), -// Visibility( -// visible: !isLocationEnabled, -// child: AppCustomChipWidget( -// labelText: "Location turned off".needTranslation, -// deleteIcon: AppAssets.location_unavailable, -// deleteIconSize: Size(9, 12), -// textColor: AppColors.blackColor, -// )), -// ], -// ); } diff --git a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart index b7a16c5..086e0da 100644 --- a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart +++ b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart @@ -1,21 +1,10 @@ -import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/enums.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/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/hospital_list_item.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class SelectHospitalBottomSheet extends StatelessWidget { diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart new file mode 100644 index 0000000..eb22ab6 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bf.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/blood_sugar.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bmi.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bmr.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/calories.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/crabs.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/dduedate.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ibw.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ovulation.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/triglycerides.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +class HealthCalculatorDetailedPage extends StatefulWidget { + HealthCalculatorsTypeEnum calculatorType; + int? clinicID; + int? calculationID; + + HealthCalculatorDetailedPage({super.key, required this.calculatorType, this.clinicID, this.calculationID}); + + @override + State createState() => _HealthCalculatorDetailedPageState(); +} + +class _HealthCalculatorDetailedPageState extends State { + dynamic calculatedResult; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => HealthCalcualtorViewModel(), + child: Consumer(builder: (context, provider, _) { + return CollapsingListView( + title: widget.calculatorType.displayName, + bottomChild: widget.calculatorType == HealthCalculatorsTypeEnum.bloodSugar || + widget.calculatorType == HealthCalculatorsTypeEnum.bloodCholesterol || + widget.calculatorType == HealthCalculatorsTypeEnum.triglycerides + ? SizedBox() + : Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + child: CustomButton( + text: widget.calculatorType == HealthCalculatorsTypeEnum.bloodSugar || + widget.calculatorType == HealthCalculatorsTypeEnum.bloodCholesterol || + widget.calculatorType == HealthCalculatorsTypeEnum.triglycerides + ? "Convert".needTranslation + : "Calculate".needTranslation, + onPressed: () { + if (calculatedResult == null) return; + + DialogService dialogService = getIt.get(); + Navigator.of(context).push(CustomPageRoute( + page: SelectClinicPage(calculatorType: widget.calculatorType, calculatedResult: calculatedResult), + )); + }, + icon: null, + fontSize: 16.f, + backgroundColor: calculatedResult == null ? AppColors.bgRedLightColor : AppColors.primaryRedColor, + borderColor: calculatedResult == null ? AppColors.bgRedLightColor : AppColors.primaryRedColor, + borderRadius: 12.r, + fontWeight: FontWeight.w500)), + child: getCalculatorWidget( + type: widget.calculatorType, + onCalculate: (result) { + // result may be directly provided by widget, but prefer provider maps if available + + switch (widget.calculatorType) { + case HealthCalculatorsTypeEnum.bmi: + calculatedResult = provider.bmiResultMap != null + ? {...?provider.bmiResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.calories: + case HealthCalculatorsTypeEnum.bmr: + calculatedResult = provider.caloriesResultMap != null + ? {...?provider.caloriesResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.idealBodyWeight: + calculatedResult = provider.ibwResultMap != null + ? {...?provider.ibwResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bodyFat: + calculatedResult = provider.bodyFatResultMap != null + ? {...?provider.bodyFatResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.crabsProteinFat: + calculatedResult = provider.macrosResultMap != null + ? {...?provider.macrosResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.ovulation: + calculatedResult = (provider.ovulationResult is Map) + ? {...provider.ovulationResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.ovulationResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.deliveryDueDate: + calculatedResult = (provider.deliveryResult is Map) + ? {...provider.deliveryResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.deliveryResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bloodSugar: + calculatedResult = (provider.bloodSugarResult is Map) + ? {...provider.bloodSugarResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.bloodSugarResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bloodCholesterol: + calculatedResult = (provider.bloodCholesterolResult is Map) + ? {...provider.bloodCholesterolResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.bloodCholesterolResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.triglycerides: + calculatedResult = (provider.triglyceridesResult is Map) + ? {...provider.triglyceridesResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.triglyceridesResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + } + // switch (widget.calculatorType) { + // case HealthCalculatorsTypeEnum.bmi: + // calculatedResult = provider.bmiResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.calories: + // case HealthCalculatorsTypeEnum.bmr: + // calculatedResult = provider.caloriesResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.idealBodyWeight: + // calculatedResult = provider.ibwResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.bodyFat: + // calculatedResult = provider.bodyFatResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.crabsProteinFat: + // calculatedResult = provider.macrosResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.ovulation: + // calculatedResult = provider.ovulationResult ?? result; + // break; + // case HealthCalculatorsTypeEnum.deliveryDueDate: + // calculatedResult = provider.deliveryResult ?? result; + // break; + // case HealthCalculatorsTypeEnum.bloodSugar: + // calculatedResult = provider.bloodSugarResult ?? result; + // case HealthCalculatorsTypeEnum.bloodCholesterol: + // calculatedResult = provider.bloodCholesterolResult ?? result; + // case HealthCalculatorsTypeEnum.triglycerides: + // calculatedResult = provider.triglyceridesResult ?? result; + // } + }, + ).paddingSymmetrical(20.w, 24.h), + ); + }), + ); + } + + Widget getCalculatorWidget({required HealthCalculatorsTypeEnum type, required Function(dynamic result) onCalculate}) { + switch (widget.calculatorType) { + case HealthCalculatorsTypeEnum.bmi: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BMIWidget(onChange: onCalculate), + ); + + case HealthCalculatorsTypeEnum.calories: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: CaloriesWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bmr: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BMRWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.idealBodyWeight: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: IdealBodyWeightWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bodyFat: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BodyFatWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.crabsProteinFat: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: CrabsWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.ovulation: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: OvulationWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.deliveryDueDate: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: DeliveryDueDWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bloodSugar: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BloodSugarWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bloodCholesterol: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BloodCholesterolWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.triglycerides: + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: TriglyceridesWidget(onChange: onCalculate), + ); + } + } +} diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart b/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart new file mode 100644 index 0000000..c085ac8 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart @@ -0,0 +1,1189 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + +class HealthCalcualtorViewModel extends ChangeNotifier { + // BMI + double? bmiResult; + String? bmiCategory; + + // BMR / Calories + double? bmrResult; + double? caloriesResult; + String? bmrRange; + String? calorieRange; + + // IBW + double? ibwResult; + double? weightDifference; + String? weightStatus; + + // Body Fat + double? bodyFatResult; + String? bodyFatCategory; + + // Crabs/Protein/Fat + double? carbsCalories; + double? proteinCalories; + double? fatCalories; + double? carbsGrams; + double? proteinGrams; + double? fatGrams; + String? dietType; + + // Ovulation + Map? ovulationResult; + + // Delivery + Map? deliveryResult; + + Map? bloodSugarResult; + Map? bloodCholesterolResult; + Map? triglyceridesResult; + + String? _mgdlValue; + + String? get mgdlValue => _mgdlValue; + + String? _mmolValue; + + String? get mmolValue => _mmolValue; + + // Store the original entered value and which field it was entered in + String? _originalValue; + String _originalUnit = 'mg/dL'; // which field the original value was entered in + bool _isSwapped = false; // toggle state for switch + + String _activeUnit = 'mg/dL'; // current source unit + String get activeUnit => _activeUnit; + + // ================== BLOOD CHOLESTEROL ================== + String? _cholMgdlValue; + String? _cholMmolValue; + String? _cholOriginalValue; + String _cholOriginalUnit = 'mg/dL'; + bool _cholIsSwapped = false; + String _cholActiveUnit = 'mg/dL'; + + String? get cholMgdlValue => _cholMgdlValue; + + String? get cholMmolValue => _cholMmolValue; + + String get cholActiveUnit => _cholActiveUnit; + + // ================== TRIGLYCERIDES ================== + String? _triMgdlValue; + String? _triMmolValue; + String? _triOriginalValue; + String _triOriginalUnit = 'mg/dL'; + bool _triIsSwapped = false; + String _triActiveUnit = 'mg/dL'; + + String? get triMgdlValue => _triMgdlValue; + + String? get triMmolValue => _triMmolValue; + + String get triActiveUnit => _triActiveUnit; + + // Generic helpers + + void calculateBMI({required String heightText, required String weightText, required String heightUnit, required String weightUnit}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + + double heightInMeters = _convertHeightToMeters(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInMeters <= 0 || weightInKg <= 0) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + return; + } + + double bmi = weightInKg / (heightInMeters * heightInMeters); + String category = _getBMICategory(bmi); + + bmiResult = bmi; + bmiCategory = category; + notifyListeners(); + } catch (e) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + } + } + + double _convertHeightToMeters(double height, String unit) { + switch (unit) { + case 'cm': + return height / 100; + case 'm': + return height; + case 'ft': + return height * 0.3048; + case 'in': + return height * 0.0254; + default: + return height / 100; + } + } + + double _convertWeightToKg(double weight, String unit) { + switch (unit) { + case 'kg': + return weight; + case 'lb': + return weight * 0.453592; + default: + return weight; + } + } + + String _getBMICategory(double bmi) { + if (bmi < 18.5) { + return 'Underweight'; + } else if (bmi >= 18.5 && bmi < 25) { + return 'Normal'; + } else if (bmi >= 25 && bmi < 30) { + return 'Overweight'; + } else { + return 'Obese'; + } + } + + // BMR / Calories (shared logic) + void calculateBMRAndCalories( + {required String heightText, + required String weightText, + required String ageText, + required String heightUnit, + required String weightUnit, + required String gender, + required String activityLevel, + bool forCaloriesOnly = false}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty || ageText.trim().isEmpty) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + int age = int.parse(ageText.trim()); + + if (age < 11 || age > 120) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + double heightInCm = _convertHeightToCm(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInCm <= 0 || weightInKg <= 0) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + double bmr = _calculateBMR(heightInCm, weightInKg, age, gender); + double activityMultiplier = _getActivityMultiplier(activityLevel); + double calories = bmr * activityMultiplier; + + bmrResult = bmr; + caloriesResult = calories; + + // ranges + if (bmr < 1200) { + bmrRange = 'Low'; + } else if (bmr < 1600) { + bmrRange = 'Normal'; + } else if (bmr < 2000) { + bmrRange = 'High'; + } else { + bmrRange = 'Very High'; + } + + if (calories < 1800) { + calorieRange = 'Low'; + } else if (calories < 2500) { + calorieRange = 'Normal'; + } else if (calories < 3200) { + calorieRange = 'High'; + } else { + calorieRange = 'High'; + } + + notifyListeners(); + } catch (e) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + } + } + + double _convertHeightToCm(double height, String unit) { + switch (unit) { + case 'cm': + return height; + case 'm': + return height * 100; + case 'ft': + return height * 30.48; + case 'in': + return height * 2.54; + default: + return height; + } + } + + double _calculateBMR(double heightCm, double weightKg, int age, String gender) { + if (gender == "Male") { + return 88.362 + (13.397 * weightKg) + (4.799 * heightCm) - (5.677 * age); + } else { + return 447.593 + (9.247 * weightKg) + (3.098 * heightCm) - (4.330 * age); + } + } + + double _getActivityMultiplier(String activityLevel) { + switch (activityLevel) { + case "Almost Inactive (no exercise)": + return 1.2; + case "Lightly active": + return 1.375; + case "Lightly active (1-3) days per week": + return 1.55; + case "Super active (very hard exercise)": + return 1.725; + default: + return 1.375; + } + } + + // IBW + void calculateIBW({required String heightText, required String weightText, required String heightUnit, required String weightUnit, required String bodyFrameSize}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + + double heightInCm = _convertHeightToCm(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInCm <= 0 || weightInKg <= 0) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + return; + } + + double ibw = _calculateIBWValue(heightInCm); + double adjusted = _applyFrameSizeAdjustment(ibw, bodyFrameSize); + double difference = weightInKg - adjusted; + String status = _getWeightStatus(difference); + + ibwResult = adjusted; + weightDifference = difference; + weightStatus = status; + notifyListeners(); + } catch (e) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + } + } + + double _calculateIBWValue(double heightCm) { + double baseHeight = 152.4; + double baseWeight = 50.0; + double heightMultiplier = 2.3; + + if (heightCm <= baseHeight) { + return baseWeight - ((baseHeight - heightCm) * 0.5); + } + + return baseWeight + ((heightCm - baseHeight) * heightMultiplier / 2.54); + } + + double _applyFrameSizeAdjustment(double ibw, String selectedBodyFrameSize) { + switch (selectedBodyFrameSize) { + case "Small (fingers overlaps)": + return ibw * 0.9; + case "Medium (fingers touch)": + return ibw; + case "Large (fingers don't touch)": + return ibw * 1.1; + default: + return ibw; + } + } + + String _getWeightStatus(double difference) { + if (difference.abs() < 2) { + return 'Normal'; + } else if (difference > 0) { + return 'Overweight'; + } else { + return 'Underweight'; + } + } + + // Body Fat + void calculateBodyFat( + {required String heightText, + required String neckText, + required String waistText, + required String hipText, + required String heightUnit, + required String neckUnit, + required String waistUnit, + required String hipUnit, + required String gender}) { + if (heightText.trim().isEmpty || neckText.trim().isEmpty || waistText.trim().isEmpty) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + if (gender == "Female" && hipText.trim().isEmpty) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double neck = double.parse(neckText.trim()); + double waist = double.parse(waistText.trim()); + double hip = gender == "Female" ? double.parse(hipText.trim()) : 0; + + double heightInCm = _convertHeightToCm(height, heightUnit); + double neckInCm = _convertLengthToCm(neck, neckUnit); + double waistInCm = _convertLengthToCm(waist, waistUnit); + double hipInCm = gender == "Female" ? _convertLengthToCm(hip, hipUnit) : 0; + + if (heightInCm <= 0 || neckInCm <= 0 || waistInCm <= 0) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + double bodyFat = _calculateBodyFatPercentage(heightInCm, neckInCm, waistInCm, hipInCm, gender == "Male"); + String category = _getBodyFatCategory(bodyFat, gender == "Male"); + + bodyFatResult = bodyFat; + bodyFatCategory = category; + notifyListeners(); + } catch (e) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + } + } + + double _convertLengthToCm(double length, String unit) { + switch (unit) { + case 'cm': + return length; + case 'm': + return length * 100; + case 'ft': + return length * 30.48; + case 'in': + return length * 2.54; + default: + return length; + } + } + + double _calculateBodyFatPercentage(double heightCm, double neckCm, double waistCm, double hipCm, bool isMale) { + if (isMale) { + double waistMinusNeck = waistCm - neckCm; + if (waistMinusNeck <= 0) return 0; + double log10WaistNeck = _log10(waistMinusNeck); + double log10Height = _log10(heightCm); + double bodyDensity = 1.0324 - (0.19077 * log10WaistNeck) + (0.15456 * log10Height); + double bodyFat = (495 / bodyDensity) - 450; + return bodyFat.clamp(0, 100); + } else { + double waistHipMinusNeck = waistCm + hipCm - neckCm; + if (waistHipMinusNeck <= 0) return 0; + double log10WaistHipNeck = _log10(waistHipMinusNeck); + double log10Height = _log10(heightCm); + double bodyDensity = 1.29579 - (0.35004 * log10WaistHipNeck) + (0.22100 * log10Height); + double bodyFat = (495 / bodyDensity) - 450; + return bodyFat.clamp(0, 100); + } + } + + double _log10(double x) { + return (x <= 0) ? 0 : (log(x) / log(10)); + } + + String _getBodyFatCategory(double bodyFat, bool isMale) { + if (isMale) { + if (bodyFat < 6) return 'Essential Fat'; + if (bodyFat < 14) return 'Athletes'; + if (bodyFat < 18) return 'Fitness'; + if (bodyFat < 25) return 'Average'; + return 'Obese'; + } else { + if (bodyFat < 14) return 'Essential Fat'; + if (bodyFat < 21) return 'Athletes'; + if (bodyFat < 25) return 'Fitness'; + if (bodyFat < 32) return 'Average'; + return 'Obese'; + } + } + + // Crabs / Macros + void calculateMacros({required String caloriesText, required String selectedDietType}) { + if (caloriesText.trim().isEmpty) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + return; + } + + try { + double totalCalories = double.parse(caloriesText.trim()); + if (totalCalories <= 0) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + return; + } + + Map macroPercentages = _getMacroPercentages(selectedDietType); + double carbsPercent = macroPercentages['carbs']!; + double proteinPercent = macroPercentages['protein']!; + double fatPercent = macroPercentages['fat']!; + + double cCalories = totalCalories * (carbsPercent / 100); + double pCalories = totalCalories * (proteinPercent / 100); + double fCalories = totalCalories * (fatPercent / 100); + + double cGrams = cCalories / 4; + double pGrams = pCalories / 4; + double fGrams = fCalories / 9; + + carbsCalories = cCalories; + proteinCalories = pCalories; + fatCalories = fCalories; + carbsGrams = cGrams; + proteinGrams = pGrams; + fatGrams = fGrams; + dietType = selectedDietType; + notifyListeners(); + } catch (e) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + } + } + + Map _getMacroPercentages(String dietType) { + switch (dietType) { + case "Very Low Crabs": + return {'carbs': 5, 'protein': 30, 'fat': 65}; + case "Low Crabs": + return {'carbs': 20, 'protein': 35, 'fat': 45}; + case "Moderate Crabs": + return {'carbs': 40, 'protein': 30, 'fat': 30}; + case "USDA Guidelines ": + return {'carbs': 55, 'protein': 15, 'fat': 30}; + case "Zone Diet": + return {'carbs': 40, 'protein': 30, 'fat': 30}; + default: + return {'carbs': 40, 'protein': 30, 'fat': 30}; + } + } + + // Ovulation + DateTime? _parseCustomDate(String dateString) { + try { + List parts = dateString.split(' '); + if (parts.length >= 3) { + int day = int.parse(parts[0]); + String month = parts[1].replaceAll(',', ''); + int year = int.parse(parts[2]); + + Map months = { + 'January': 1, + 'February': 2, + 'March': 3, + 'April': 4, + 'May': 5, + 'June': 6, + 'July': 7, + 'August': 8, + 'September': 9, + 'October': 10, + 'November': 11, + 'December': 12, + 'Jan': 1, + 'Feb': 2, + 'Mar': 3, + 'Apr': 4, + 'Jun': 6, + 'Jul': 7, + 'Aug': 8, + 'Sep': 9, + 'Oct': 10, + 'Nov': 11, + 'Dec': 12, + }; + + int? monthNum = months[month]; + if (monthNum != null) { + return DateTime(year, monthNum, day); + } + } + return null; + } catch (e) { + return null; + } + } + + DateTime? _tryParseDate(String dateString) { + try { + // try ISO like + return DateTime.tryParse(dateString) ?? _parseCustomDate(dateString); + } catch (e) { + return null; + } + } + + String _formatDate(DateTime? date) { + if (date == null) return ''; + String day = date.day.toString().padLeft(2, '0'); + return '$day ${_getMonthName(date.month)}, ${date.year}'; + } + + String _getMonthName(int month) { + const List months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + return months[month - 1]; + } + + String _getDayName(int weekday) { + // DateTime.weekday returns 1 for Monday and 7 for Sunday. + const List days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + return days[weekday - 1]; + } + + String _formatDateWithDayName(DateTime? date) { + if (date == null) return ''; + String dayName = _getDayName(date.weekday); // Gets 'Monday', 'Tuesday', etc. + return dayName; + } + + Map convertBloodSugar(double value, String fromUnit) { + if (fromUnit == 'mg/dL') { + final mmolL = value / 18.0182; + return {'mgdL': value, 'mmolL': mmolL}; + } else if (fromUnit == 'mmol/L') { + final mgdL = value * 18.0182; + return {'mgdL': mgdL, 'mmolL': value}; + } + return {'mgdL': 0, 'mmolL': 0}; + } + + String _determineCyclePhase(int daysSincePeriod, int follicularPhase, int cycleLength) { + int normalizedDay = daysSincePeriod % cycleLength; + + if (normalizedDay < follicularPhase - 1) { + return 'Follicular Phase'; + } else if (normalizedDay < follicularPhase + 2) { + return 'Ovulation'; + } else if (normalizedDay < cycleLength) { + return 'Luteal Phase'; + } else { + return 'Menstruation'; + } + } + + void calculateOvulation({required String dateText, required String cycleLengthText, required String lutealPhaseText}) { + if (dateText.trim().isEmpty || cycleLengthText.trim().isEmpty || lutealPhaseText.trim().isEmpty) { + ovulationResult = null; + notifyListeners(); + return; + } + + DateTime? parsedDate = _tryParseDate(dateText); + if (parsedDate == null) { + ovulationResult = null; + notifyListeners(); + return; + } + + try { + int cycleLength = int.parse(cycleLengthText.trim()); + int lutealPhase = int.parse(lutealPhaseText.trim()); + + if (cycleLength < 16 || cycleLength > 40 || lutealPhase < 8 || lutealPhase > 16) { + ovulationResult = null; + notifyListeners(); + return; + } + + if (cycleLength <= lutealPhase) { + ovulationResult = null; + notifyListeners(); + return; + } + + int follicularPhase = cycleLength - lutealPhase; + DateTime ovulationDate = parsedDate.add(Duration(days: follicularPhase - 1)); + DateTime fertileStart = ovulationDate.subtract(Duration(days: 5)); + DateTime fertileEnd = ovulationDate.add(Duration(days: 1)); + int daysSincePeriod = DateTime.now().difference(parsedDate).inDays; + String cyclePhase = _determineCyclePhase(daysSincePeriod, follicularPhase, cycleLength); + + ovulationResult = { + 'lastPeriodDay': _formatDateWithDayName(parsedDate), + 'ovulationDay': _formatDateWithDayName(ovulationDate), + 'lastPeriodDate': _formatDate(parsedDate), + 'ovulationDate': _formatDate(ovulationDate), + 'fertileWindowStart': _formatDate(fertileStart), + 'fertileWindowEnd': _formatDate(fertileEnd), + 'cycleLength': cycleLength, + 'lutealPhase': lutealPhase, + 'follicularPhase': follicularPhase, + 'cyclePhase': cyclePhase, + 'nextPeriodDate': _formatDate(parsedDate.add(Duration(days: cycleLength))), + }; + + notifyListeners(); + } catch (e) { + ovulationResult = null; + notifyListeners(); + } + } + + // Delivery due date + void calculateDueDate({required String isoDateString}) { + if (isoDateString.trim().isEmpty) { + deliveryResult = null; + notifyListeners(); + return; + } + + DateTime? lmp; + try { + lmp = DateTime.tryParse(isoDateString) ?? _tryParseDate(isoDateString); + } catch (_) { + lmp = null; + } + + if (lmp == null) { + deliveryResult = null; + notifyListeners(); + return; + } + + // Pregnancy constants + const int totalPregnancyDays = 280; + + // Calculate main dates + final DateTime dueDate = lmp.add(const Duration(days: totalPregnancyDays)); + + // Trimester calculations + final DateTime firstTrimesterStart = lmp; + final DateTime firstTrimesterEnd = lmp.add(const Duration(days: 83)); + + final DateTime secondTrimesterStart = firstTrimesterEnd.add(const Duration(days: 1)); + final DateTime secondTrimesterEnd = lmp.add(const Duration(days: 195)); + + final DateTime thirdTrimesterStart = secondTrimesterEnd.add(const Duration(days: 1)); + final DateTime thirdTrimesterEnd = dueDate; + + deliveryResult = { + 'lmpDate': _formatDate(lmp), + 'dueDate': _formatDate(dueDate), + // Raw DateTime (useful for logic/UI) + 'lmpDateTime': lmp, + 'dueDateTime': dueDate, + 'dueDateDay': _formatDateWithDayName(dueDate), + // Trimester info + 'firstTrimester': { + 'start': _formatDate(firstTrimesterStart), + 'end': _formatDate(firstTrimesterEnd), + 'weeks': '1–12', + 'startDateTime': firstTrimesterStart, + 'endDateTime': firstTrimesterEnd, + }, + 'secondTrimester': { + 'start': _formatDate(secondTrimesterStart), + 'end': _formatDate(secondTrimesterEnd), + 'weeks': '13–27', + 'startDateTime': secondTrimesterStart, + 'endDateTime': secondTrimesterEnd, + }, + 'thirdTrimester': { + 'start': _formatDate(thirdTrimesterStart), + 'end': _formatDate(thirdTrimesterEnd), + 'weeks': '28–40', + 'startDateTime': thirdTrimesterStart, + 'endDateTime': thirdTrimesterEnd, + }, + }; + + notifyListeners(); + } + + // Blood sugar conversions + void calculateBloodSugar({required String valueText, required String unit}) { + if (valueText.trim().isEmpty) { + bloodSugarResult = null; + notifyListeners(); + return; + } + + try { + double value = double.parse(valueText.trim()); + + if (value <= 0) { + bloodSugarResult = null; + notifyListeners(); + return; + } + + double converted; + String convertedUnit; + + if (unit == 'mg/dL' || unit == 'MG/DL' || unit.toLowerCase().contains('mg')) { + // mg/dL -> mmol/L (glucose): divide by 18.0182 + converted = value / 18.0182; + convertedUnit = 'mmol/L'; + } else { + // mmol/L -> mg/dL + converted = value * 18.0182; + convertedUnit = 'mg/dL'; + } + + // round to sensible precision + double convertedRounded = double.parse(converted.toStringAsFixed(2)); + + bloodSugarResult = { + 'input': value, + 'inputUnit': unit, + 'converted': convertedRounded, + 'convertedUnit': convertedUnit, + }; + notifyListeners(); + } catch (e) { + bloodSugarResult = null; + notifyListeners(); + } + } + + // expose map-like results for widgets to forward to parent + Map? get bmiResultMap => bmiResult == null ? null : {'bmiResult': bmiResult, 'bmiCategory': bmiCategory}; + + Map? get caloriesResultMap => caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; + + Map? get ibwResultMap => ibwResult == null ? null : {'ibw': ibwResult, 'difference': weightDifference, 'status': weightStatus}; + + Map? get bodyFatResultMap => bodyFatResult == null ? null : {'fatPercentage': bodyFatResult, 'fatCategory': bodyFatCategory}; + + Map? get macrosResultMap => carbsCalories == null + ? null + : { + 'totalCalories': carbsCalories! + proteinCalories! + fatCalories!, + 'carbsGrams': carbsGrams, + 'carbsCalories': carbsCalories, + 'proteinGrams': proteinGrams, + 'proteinCalories': proteinCalories, + 'fatGrams': fatGrams, + 'fatCalories': fatCalories, + 'dietType': dietType + }; + + void onBloodSugarChanged(String value, String fromUnit) { + _activeUnit = fromUnit; + + if (value.isEmpty) { + _mgdlValue = null; + _mmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _mgdlValue = value; + _mmolValue = (parsed / 18.0182).toStringAsFixed(3); + } else { + _mmolValue = value; + _mgdlValue = (parsed * 18.0182).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onBloodSugarMgdlChanged(String value) { + _mgdlValue = value; + _originalValue = value; + _originalUnit = 'mg/dL'; + _isSwapped = false; + _activeUnit = 'mg/dL'; + + if (value.isEmpty) { + _mmolValue = null; + _originalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _mmolValue = (parsed / 18.0182).toStringAsFixed(3); + notifyListeners(); + } + + void onBloodSugarMmolChanged(String value) { + _mmolValue = value; + _originalValue = value; + _originalUnit = 'mmol/L'; + _isSwapped = false; + _activeUnit = 'mmol/L'; + + if (value.isEmpty) { + _mgdlValue = null; + _originalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _mgdlValue = (parsed * 18.0182).toStringAsFixed(3); + notifyListeners(); + } + + void switchBloodSugarValues() { + // Toggle between two states using the original entered value + if (_originalValue == null || _originalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_originalValue!); + if (originalParsed == null) return; + + _isSwapped = !_isSwapped; + + if (_originalUnit == 'mg/dL') { + if (_isSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _mmolValue = _originalValue; // Keep original format + _mgdlValue = (originalParsed * 18.0182).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _mgdlValue = _originalValue; // Keep original format + _mmolValue = (originalParsed / 18.0182).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_isSwapped) { + // Now treat it as mg/dL + _mgdlValue = _originalValue; // Keep original format + _mmolValue = (originalParsed / 18.0182).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _mmolValue = _originalValue; // Keep original format + _mgdlValue = (originalParsed * 18.0182).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + // --- NEW: Method to clear the values --- + void clearBloodSugar() { + _mgdlValue = null; + _mmolValue = null; + _originalValue = null; + _originalUnit = 'mg/dL'; + _isSwapped = false; + _activeUnit = 'mg/dL'; + notifyListeners(); + } + + void onBloodCholesterolChanged(String value, String fromUnit) { + _cholActiveUnit = fromUnit; + + if (value.isEmpty) { + _cholMgdlValue = null; + _cholMmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _cholMgdlValue = value; + _cholMmolValue = (parsed / 38.67).toStringAsFixed(3); + } else { + _cholMmolValue = value; + _cholMgdlValue = (parsed * 38.67).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onCholesterolMgdlChanged(String value) { + _cholMgdlValue = value; + _cholOriginalValue = value; + _cholOriginalUnit = 'mg/dL'; + _cholIsSwapped = false; + _cholActiveUnit = 'mg/dL'; + + if (value.isEmpty) { + _cholMmolValue = null; + _cholOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _cholMmolValue = (parsed / 38.67).toStringAsFixed(3); + notifyListeners(); + } + + void onCholesterolMmolChanged(String value) { + _cholMmolValue = value; + _cholOriginalValue = value; + _cholOriginalUnit = 'mmol/L'; + _cholIsSwapped = false; + _cholActiveUnit = 'mmol/L'; + + if (value.isEmpty) { + _cholMgdlValue = null; + _cholOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _cholMgdlValue = (parsed * 38.67).toStringAsFixed(3); + notifyListeners(); + } + + void switchBloodCholesterolValues() { + // Toggle between two states using the original entered value + if (_cholOriginalValue == null || _cholOriginalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_cholOriginalValue!); + if (originalParsed == null) return; + + _cholIsSwapped = !_cholIsSwapped; + + if (_cholOriginalUnit == 'mg/dL') { + if (_cholIsSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _cholMmolValue = _cholOriginalValue; // Keep original format + _cholMgdlValue = (originalParsed * 38.67).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _cholMgdlValue = _cholOriginalValue; // Keep original format + _cholMmolValue = (originalParsed / 38.67).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_cholIsSwapped) { + // Now treat it as mg/dL + _cholMgdlValue = _cholOriginalValue; // Keep original format + _cholMmolValue = (originalParsed / 38.67).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _cholMmolValue = _cholOriginalValue; // Keep original format + _cholMgdlValue = (originalParsed * 38.67).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + void clearBloodCholesterol() { + _cholMgdlValue = null; + _cholMmolValue = null; + _cholOriginalValue = null; + _cholOriginalUnit = 'mg/dL'; + _cholIsSwapped = false; + _cholActiveUnit = 'mg/dL'; + notifyListeners(); + } + + void onTriglyceridesChanged(String value, String fromUnit) { + _triActiveUnit = fromUnit; + + if (value.isEmpty) { + _triMgdlValue = null; + _triMmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _triMgdlValue = value; + _triMmolValue = (parsed / 88.57).toStringAsFixed(3); + } else { + _triMmolValue = value; + _triMgdlValue = (parsed * 88.57).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onTriglyceridesMgdlChanged(String value) { + _triMgdlValue = value; + _triOriginalValue = value; + _triOriginalUnit = 'mg/dL'; + _triIsSwapped = false; + _triActiveUnit = 'mg/dL'; + + if (value.isEmpty) { + _triMmolValue = null; + _triOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _triMmolValue = (parsed / 88.57).toStringAsFixed(3); + notifyListeners(); + } + + void onTriglyceridesMmolChanged(String value) { + _triMmolValue = value; + _triOriginalValue = value; + _triOriginalUnit = 'mmol/L'; + _triIsSwapped = false; + _triActiveUnit = 'mmol/L'; + + if (value.isEmpty) { + _triMgdlValue = null; + _triOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _triMgdlValue = (parsed * 88.57).toStringAsFixed(3); + notifyListeners(); + } + + void switchTriglyceridesValues() { + // Toggle between two states using the original entered value + if (_triOriginalValue == null || _triOriginalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_triOriginalValue!); + if (originalParsed == null) return; + + _triIsSwapped = !_triIsSwapped; + + if (_triOriginalUnit == 'mg/dL') { + if (_triIsSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _triMmolValue = _triOriginalValue; // Keep original format + _triMgdlValue = (originalParsed * 88.57).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _triMgdlValue = _triOriginalValue; // Keep original format + _triMmolValue = (originalParsed / 88.57).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_triIsSwapped) { + // Now treat it as mg/dL + _triMgdlValue = _triOriginalValue; // Keep original format + _triMmolValue = (originalParsed / 88.57).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _triMmolValue = _triOriginalValue; // Keep original format + _triMgdlValue = (originalParsed * 88.57).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + void clearTriglycerides() { + _triMgdlValue = null; + _triMmolValue = null; + _triOriginalValue = null; + _triOriginalUnit = 'mg/dL'; + _triIsSwapped = false; + _triActiveUnit = 'mg/dL'; + notifyListeners(); + } +} diff --git a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart new file mode 100644 index 0000000..faf97a1 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/health_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/routes/custom_page_route.dart'; + +class HealthCalculatorsPage extends StatefulWidget { + HealthCalConEnum type; + + HealthCalculatorsPage({super.key, required this.type}); + + @override + State createState() => _HealthCalculatorsPageState(); +} + +class _HealthCalculatorsPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt.get(); + return CollapsingListView( + title: widget.type == HealthCalConEnum.calculator ? "Health Calculators".needTranslation : "Health Converters".needTranslation, + child: widget.type == HealthCalConEnum.calculator + ? Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "General Health".needTranslation.toText16(weight: FontWeight.w600), + "Related To BMI, calories, body fat, etc to stay updated with your health.".needTranslation.toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox( + width: 12.w, + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Calculator".needTranslation, + message: "", + child: showCalculatorsItems(type: HealthCalculatorEnum.general), + onOkPressed: () {}, + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Women's Health".needTranslation.toText16(weight: FontWeight.w600), + "Related To periods, ovulation, pregnancy, and other topics.".needTranslation.toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Calculator".needTranslation, + message: "", + child: showCalculatorsItems(type: HealthCalculatorEnum.women), + onOkPressed: () {}, + ); + }), + ], + ).paddingSymmetrical(20.w, 24.h) + : Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bloodSugar, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Blood Sugar".needTranslation.toText16(weight: FontWeight.w600), + "Track your glucose levels, understand trends, and get personalized insights for better health.".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: Color(0xFF8F9AA3), + ) + ], + ), + ), + SizedBox( + width: 12.w, + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.bloodSugar), + ), + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bloodCholestrol, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Blood Cholesterol".needTranslation.toText16(weight: FontWeight.w600), + "Monitor your cholesterol levels, track your LDL, HDL, and triglycerides. Get personalized recommendations for a healthy heart." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.bloodCholesterol), + ), + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.triglycerides, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Triglycerides Fat Blood".needTranslation.toText16(weight: FontWeight.w600), + "Manage triglycerides, a key blood fat. Understand levels, diet impacts, and heart health strategies." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.triglycerides), + ), + ); + }), + ], + ).paddingSymmetrical(20.w, 24.h)); + } + + Widget showCalculatorsItems({required HealthCalculatorEnum type}) { + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, // 4 icons per row + crossAxisSpacing: 16.w, + mainAxisSpacing: 16.w, + childAspectRatio: 0.80), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return HealthCard( + icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon, + labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage( + calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type, + clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID, + calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID, + ), + ), + ); + }, + ); + }, + ); + } + + final List generalHealthServices = [ + HealthComponentModel(title: "BMI\nCalculator".needTranslation, icon: AppAssets.bmi, type: HealthCalculatorsTypeEnum.bmi, clinicID: 108, calculationID: null), + HealthComponentModel(title: "Calories\nCalculator".needTranslation, icon: AppAssets.calories, type: HealthCalculatorsTypeEnum.calories, clinicID: null, calculationID: 2), + HealthComponentModel(title: "BMR\nCalculator".needTranslation, icon: AppAssets.bmr, type: HealthCalculatorsTypeEnum.bmr, clinicID: null, calculationID: 3), + HealthComponentModel(title: "Ideal Body\nWeight".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.idealBodyWeight, clinicID: null, calculationID: 4), + HealthComponentModel(title: "Body Fat\nCalculator".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.bodyFat, clinicID: null, calculationID: 5), + HealthComponentModel(title: "Carbs\nProtein & Fat".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.crabsProteinFat, clinicID: null, calculationID: 11), + ]; + + final List womenHealthServices = [ + HealthComponentModel(title: "Ovulation\nPeriod".needTranslation, icon: AppAssets.locate_me, type: HealthCalculatorsTypeEnum.ovulation, clinicID: null, calculationID: 6 ), + HealthComponentModel(title: "Delivery\nDue Date".needTranslation, icon: AppAssets.activeCheck, type: HealthCalculatorsTypeEnum.deliveryDueDate, clinicID: null, calculationID: 6), + ]; +} + +class HealthComponentModel { + String title; + String? subTitle; + String icon; + Color? iconColor; + Color? bgColor; + Color? textColor; + HealthCalculatorsTypeEnum type; + int? clinicID; + int? calculationID; + + HealthComponentModel({required this.title, this.subTitle, required this.icon, this.iconColor, this.bgColor, this.textColor, required this.type, this.clinicID, this.calculationID}); +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bf.dart b/lib/presentation/health_calculators_and_converts/widgets/bf.dart new file mode 100644 index 0000000..3a88eb3 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bf.dart @@ -0,0 +1,641 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class BodyFatWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BodyFatWidget({super.key, required this.onChange}); + + @override + _BodyFatWidgetState createState() => _BodyFatWidgetState(); +} + +class _BodyFatWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _neckController = TextEditingController(); + final TextEditingController _waistController = TextEditingController(); + final TextEditingController _hipController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + String selectedHeightUnit = 'cm'; + String selectedNeckUnit = 'in'; + String selectedWaistUnit = 'in'; + String selectedHipUnit = 'in'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _neckController.addListener(_onInputChanged); + _waistController.addListener(_onInputChanged); + _hipController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _neckController.removeListener(_onInputChanged); + _waistController.removeListener(_onInputChanged); + _hipController.removeListener(_onInputChanged); + _heightController.dispose(); + _neckController.dispose(); + _waistController.dispose(); + _hipController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBodyFat( + heightText: _heightController.text, + neckText: _neckController.text, + waistText: _waistController.text, + hipText: _hipController.text, + heightUnit: selectedHeightUnit, + neckUnit: selectedNeckUnit, + waistUnit: selectedWaistUnit, + hipUnit: selectedHipUnit, + gender: selectedGender, + ); + if (widget.onChange != null) widget.onChange!(provider.bodyFatResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Neck".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _neckController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '7', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedNeckUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedNeckUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedNeckUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedNeckUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Waist".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _waistController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '30', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWaistUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWaistUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWaistUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWaistUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Hip".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _hipController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '0', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHipUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHipUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHipUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Estimate the total body fat based on the size.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart b/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart new file mode 100644 index 0000000..e3bc7ff --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BloodCholesterolWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const BloodCholesterolWidget({super.key, this.onChange}); + + @override + State createState() => _BloodCholesterolWidgetState(); +} + +class _BloodCholesterolWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearBloodCholesterol(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + final mgdlText = provider.cholMgdlValue ?? ''; + final mmolText = provider.cholMmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "120", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onCholesterolMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchBloodCholesterolValues(); + }), + ], + ), + _buildInputField( + label: "MMOL/L", + hint: "3.1", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onCholesterolMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Convert blood cholesterol values between mg/dL and mmol/L. ".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + /// Reusable Input Field + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + maxLines: 1, + onChanged: onChanged, + cursorHeight: 35.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.h, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart b/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart new file mode 100644 index 0000000..cf7cc61 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BloodSugarWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const BloodSugarWidget({super.key, this.onChange}); + + @override + State createState() => _BloodSugarWidgetState(); +} + +class _BloodSugarWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearBloodSugar(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + final mgdlText = provider.mgdlValue ?? ''; + final mmolText = provider.mmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "120", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onBloodSugarMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchBloodSugarValues(); + }), + ], + ), + _buildInputField( + label: "MMOL/L", + hint: "6.7", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onBloodSugarMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.globe, + width: 18.w, + height: 18.w, + ), + SizedBox(width: 12.w), + Expanded( + child: "Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)".toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + /// Reusable Input Field + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + maxLines: 1, + onChanged: onChanged, + cursorHeight: 35.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.h, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmi.dart b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart new file mode 100644 index 0000000..49b77c0 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BMIWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BMIWidget({super.key, required this.onChange}); + + @override + _BMIWidgetState createState() => _BMIWidgetState(); +} + +class _BMIWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMI(heightText: _heightController.text, weightText: _weightController.text, heightUnit: selectedHeightUnit, weightUnit: selectedWeightUnit); + // notify parent + if (widget.onChange != null) widget.onChange!(provider.bmiResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toUpperCase().toText12(fontWeight: FontWeight.w600, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + _onInputChanged(); + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toUpperCase().toText12(fontWeight: FontWeight.w600, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate the BMI value and weight status to identify the healthy weight. NOt appropriate for children and women who are pregnant or breastfeeding." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmr.dart b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart new file mode 100644 index 0000000..5f429ff --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart @@ -0,0 +1,522 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BMRWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BMRWidget({super.key, required this.onChange}); + + @override + _BMRWidgetState createState() => _BMRWidgetState(); +} + +class _BMRWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final TextEditingController _ageController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedActivityLevel = 'Lightly active'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + _ageController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _ageController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + _ageController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMRAndCalories( + heightText: _heightController.text, + weightText: _weightController.text, + ageText: _ageController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + gender: selectedGender, + activityLevel: selectedActivityLevel, + ); + if (widget.onChange != null) widget.onChange!(provider.caloriesResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + AppState appState = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column(children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + print(value); + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.of(context).pop(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + }); + _onInputChanged(); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Age (11-120) yrs".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _ageController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '20', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Activity Level".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedActivityLevel.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Activity Level".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedActivityLevel, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedActivityLevel = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedActivityLevel = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates daily calorie intake based on server factors, like height, weight, age, gender and daily physical activity." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ])); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/calories.dart b/lib/presentation/health_calculators_and_converts/widgets/calories.dart new file mode 100644 index 0000000..fbe15a6 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/calories.dart @@ -0,0 +1,523 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class CaloriesWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + CaloriesWidget({super.key, required this.onChange}); + + @override + _CaloriesWidgetState createState() => _CaloriesWidgetState(); +} + +class _CaloriesWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final TextEditingController _ageController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedActivityLevel = 'Lightly active'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + _ageController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _ageController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + _ageController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMRAndCalories( + heightText: _heightController.text, + weightText: _weightController.text, + ageText: _ageController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + gender: selectedGender, + activityLevel: selectedActivityLevel, + ); + if (widget.onChange != null) widget.onChange!(provider.caloriesResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + AppState appState = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.of(context).pop(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Age (11-120) yrs".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _ageController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '20', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Activity Level".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedActivityLevel.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Activity Level".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedActivityLevel, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedActivityLevel = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedActivityLevel = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates daily calorie intake based on server factors, like height, weight, age, gender and daily physical activity." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/crabs.dart b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart new file mode 100644 index 0000000..5fe1215 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class CrabsWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + CrabsWidget({super.key, required this.onChange}); + + @override + _CrabsWidgetState createState() => _CrabsWidgetState(); +} + +class _CrabsWidgetState extends State { + final TextEditingController _caloriesController = TextEditingController(); + String selectedDietType = 'Very Low Crabs'; + Function(String)? onUnitSelected; + + @override + void initState() { + super.initState(); + _caloriesController.addListener(_onInputChanged); + } + + @override + void dispose() { + _caloriesController.removeListener(_onInputChanged); + _caloriesController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateMacros(caloriesText: _caloriesController.text, selectedDietType: selectedDietType); + if (widget.onChange != null) widget.onChange!(provider.macrosResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Calories Per Day".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _caloriesController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '2000', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.ask_doctor_icon).onPress(() { + Navigator.pop(context); + Navigator.of(context).push(CustomPageRoute(page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.calories))); + }), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Diet Type".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedDietType.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Very Low Crabs", "Low Crabs", "Moderate Crabs", "USDA Guidelines ", "Zone Diet"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Diet Type".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedDietType, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedDietType = value; + _onInputChanged(); + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedDietType = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart b/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart new file mode 100644 index 0000000..15c0a0c --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class DeliveryDueDWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + DeliveryDueDWidget({super.key, required this.onChange}); + + @override + _DeliveryDueDWidgetState createState() => _DeliveryDueDWidgetState(); +} + +class _DeliveryDueDWidgetState extends State { + final TextEditingController _date = TextEditingController(); + String selectedDate = ''; + Function(String)? onUnitSelected; + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 0.h, bottom: 0.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + TextInputWidget( + labelText: "Last Period Date", + hintText: "11 July, 1994".needTranslation, + controller: _date, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + + setState(() { + selectedDate = Utils.formatDateToDisplay(val); + _date.text = selectedDate; + }); + provider.calculateDueDate(isoDateString: val); + + if (widget.onChange != null) { + widget.onChange!(provider.deliveryResult); + } + }, + ).withVerticalPadding(4.h), + ], + ), + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/health_card.dart b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart new file mode 100644 index 0000000..d54d037 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthCard extends StatelessWidget { + const HealthCard({ + super.key, + required this.icon, + required this.labelText, + this.onTap, + }); + + final String icon; + final String labelText; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: icon, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).toShimmer2(isShow: false, radius: 12.r), + SizedBox(height: 14.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: labelText.toText12( + fontWeight: FontWeight.w600, + maxLine: 2, + )), + ], + ), + ], + ).paddingAll(16.w).onPress(() { + onTap!(); + }), + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/ibw.dart b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart new file mode 100644 index 0000000..5f67511 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart @@ -0,0 +1,393 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class IdealBodyWeightWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + IdealBodyWeightWidget({super.key, required this.onChange}); + + @override + _IdealBodyWeightWidgetState createState() => _IdealBodyWeightWidgetState(); +} + +class _IdealBodyWeightWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedBodyFrameSize = 'Medium (fingers touch)'; + Function(String)? onUnitSelected; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateIBW( + heightText: _heightController.text, + weightText: _weightController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + bodyFrameSize: selectedBodyFrameSize, + ); + if (widget.onChange != null) widget.onChange!(provider.ibwResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Body Frame Size".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedBodyFrameSize.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Small (fingers overlaps)", "Medium (fingers touch)", "Large (fingers don't touch)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Body Frame Size".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedBodyFrameSize, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedBodyFrameSize = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedBodyFrameSize = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates the ideal body weight based on height, weight and body size.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart new file mode 100644 index 0000000..de209cb --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class OvulationWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + OvulationWidget({super.key, required this.onChange}); + + @override + _OvulationWidgetState createState() => _OvulationWidgetState(); +} + +class _OvulationWidgetState extends State { + final TextEditingController _ageController = TextEditingController(); + final TextEditingController _cycleLengthController = TextEditingController(); + final TextEditingController _lutealPhaseLengthController = TextEditingController(); + Function(String)? onUnitSelected; + String selectedDate = ''; + + @override + void initState() { + super.initState(); + _ageController.addListener(_onInputChanged); + _cycleLengthController.addListener(_onInputChanged); + _lutealPhaseLengthController.addListener(_onInputChanged); + } + + @override + void dispose() { + _ageController.removeListener(_onInputChanged); + _cycleLengthController.removeListener(_onInputChanged); + _lutealPhaseLengthController.removeListener(_onInputChanged); + _ageController.dispose(); + _cycleLengthController.dispose(); + _lutealPhaseLengthController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + final dateText = selectedDate.isEmpty ? _ageController.text.trim() : selectedDate; + provider.calculateOvulation(dateText: dateText, cycleLengthText: _cycleLengthController.text, lutealPhaseText: _lutealPhaseLengthController.text); + if (widget.onChange != null) widget.onChange!(provider.ovulationResult); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + TextInputWidget( + labelText: "Date", + hintText: "11 July, 1994".needTranslation, + controller: _ageController, + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + setState(() { + _ageController.text = selectedDate = Utils.formatDateToDisplay(val); + }); + _onInputChanged(); + }, + ).withVerticalPadding(8), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Average Cycle Length (Usually 28 days)".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _cycleLengthController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '28', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Average Luteal Phase Length(Usually 14 days)".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _lutealPhaseLengthController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '14', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate ovulation and fertile window based on last period, cycle length and luteal phase.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart b/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart new file mode 100644 index 0000000..1c8c99e --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class TriglyceridesWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const TriglyceridesWidget({super.key, this.onChange}); + + @override + State createState() => _TriglyceridesWidgetState(); +} + +class _TriglyceridesWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearTriglycerides(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + + final mgdlText = provider.triMgdlValue ?? ''; + final mmolText = provider.triMmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "150", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onTriglyceridesMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchTriglyceridesValues(); + }), + ], + ), + + _buildInputField( + label: "MMOL/L", + hint: "1.7", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onTriglyceridesMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + + const Divider(height: 1, color: Color(0xFFEEEEEE)), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.globe, + width: 18.w, + height: 18.w, + ), + SizedBox(width: 12.w), + Expanded( + child: + "Convert triglyceride values between mg/dL and mmol/L." + .toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: onChanged, + cursorHeight: 35.h, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index f79aae0..3b31d38 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -1,22 +1,587 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; +import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; +import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; +import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; +import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +import '../../core/dependencies.dart' show getIt; class ServicesPage extends StatelessWidget { - const ServicesPage({super.key}); + ServicesPage({super.key}); + + late BloodDonationViewModel bloodDonationViewModel; + late MedicalFileViewModel medicalFileViewModel; + + late final List hmgServices = [ + HmgServicesComponentModel( + 11, + "Emergency Services".needTranslation, + "".needTranslation, + AppAssets.emergency_services_icon, + bgColor: AppColors.primaryRedColor, + true, route: null, onTap: () { + getIt.get().flushData(); + getIt.get().getTransportationOrders( + showLoader: false, + ); + getIt.get().getRRTOrders( + showLoader: false, + ); + Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( + CustomPageRoute( + page: EmergencyServicesPage(), + settings: const RouteSettings(name: '/EmergencyServicesPage'), + ), + ); + }), + HmgServicesComponentModel( + 11, + "Book\nAppointment".needTranslation, + "".needTranslation, + AppAssets.appointment_calendar_icon, + bgColor: AppColors.bookAppointment, + true, + route: AppRoutes.bookAppointmentPage, + ), + HmgServicesComponentModel( + 5, + "Complete Checkup".needTranslation, + "".needTranslation, + AppAssets.comprehensiveCheckup, + bgColor: AppColors.bgGreenColor, + true, + route: AppRoutes.comprehensiveCheckupPage, + ), + HmgServicesComponentModel( + 11, + "E-Referral Services".needTranslation, + "".needTranslation, + AppAssets.eReferral, + bgColor: AppColors.eReferralCardColor, + true, + route: AppRoutes.eReferralPage, + ), + HmgServicesComponentModel( + 3, + "Blood Donation".needTranslation, + "".needTranslation, + AppAssets.blood_donation_icon, + bgColor: AppColors.bloodDonationCardColor, + true, + route: null, onTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Data..."); + await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async { + // await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BloodDonationPage(), + ), + ); + // }, onError: (err) { + // LoaderBottomSheet.hideLoader(); + // }); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + }); + }), + // HmgServicesComponentModel( + // 11, + // "Covid 19 Test".needTranslation, + // "".needTranslation, + // AppAssets.covid19icon, + // bgColor: AppColors.covid29Color, + // true, + // route: AppRoutes.covid19Test, + // ), + + // HmgServicesComponentModel( + // 11, + // "Vital Sign".needTranslation, + // "".needTranslation, + // AppAssets.covid19icon, + // bgColor: AppColors.covid29Color, + // true, + // route: AppRoutes.vitalSign, + // ) + + // HmgServicesComponentModel( + // 3, + // "Home Health Care".needTranslation, + // "".needTranslation, + // AppAssets.homeBottom, + // bgColor: AppColors.primaryRedColor, + // true, + // route: AppRoutes.homeHealthCarePage, + // ), + // HmgServicesComponentModel( + // 11, + // "Virtual Tour".needTranslation, + // "".needTranslation, + // AppAssets.my_address, + // bgColor: AppColors.quickLoginColor, + // true, + // route: null, + // onTap:(){ + // Utils.openWebView( + // url: 'https://hmgwebservices.com/vt_mobile/html/index.html', + // ); + // }, + // ) + ]; + + late final List hmgHealthToolServices = [ + HmgServicesComponentModel( + 11, + "Daily Water Monitor".needTranslation, + "".needTranslation, + AppAssets.daily_water_monitor_icon, + bgColor: AppColors.whiteColor, + true, + route: null, // Set to null since we handle navigation in onTap + onTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Fetching your water intake details.".needTranslation); + final waterMonitorVM = getIt.get(); + final context = getIt.get().navigatorKey.currentContext!; + await waterMonitorVM.fetchUserDetailsForMonitoring( + onSuccess: (userDetail) { + LoaderBottomSheet.hideLoader(); + if (userDetail == null) { + waterMonitorVM.populateFromAuthenticatedUser(); + context.navigateWithName(AppRoutes.waterMonitorSettingsScreen); + } else { + context.navigateWithName(AppRoutes.waterConsumptionScreen); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.waterConsumptionScreen); + }, + ); + }, + ), + HmgServicesComponentModel( + 11, + "Health\nCalculators".needTranslation, + "".needTranslation, + AppAssets.health_calculators_services_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthCalculatorsPage, + ), + HmgServicesComponentModel( + 5, + "Health\nConverters".needTranslation, + "".needTranslation, + AppAssets.health_converters_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthConvertersPage, + ), + HmgServicesComponentModel( + 11, + "Smart\nWatches".needTranslation, + "".needTranslation, + AppAssets.smartwatch_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.smartWatches, + // route: AppRoutes.huaweiHealthExample, + ), + ]; @override Widget build(BuildContext context) { - return CollapsingListView( - title: "Explore Services".needTranslation, - isLeading: false, - child: Padding( - padding: EdgeInsets.all(24.h), + bloodDonationViewModel = Provider.of(context); + medicalFileViewModel = Provider.of(context); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Explore Services".needTranslation, + isLeading: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "Medical & Care Services".needTranslation.toText18(isBold: true) + SizedBox(height: 16.h), + "Medical & Care Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row + crossAxisSpacing: 12.w, + mainAxisSpacing: 18.h, + childAspectRatio: 0.8, + ), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: hmgServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return ServiceGridViewItem(hmgServices[index], index, false, isHealthToolIcon: false); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "HMG Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + SizedBox( + height: 350.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getServiceCardsList.length, + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: LargeServiceCard( + serviceCardData: LandingPageData.getServiceCardsList[index], + image: LandingPageData.getServiceCardsList[index].icon, + title: LandingPageData.getServiceCardsList[index].title, + subtitle: LandingPageData.getServiceCardsList[index].subtitle, + icon: LandingPageData.getServiceCardsList[index].largeCardIcon, + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), + ), + ), + SizedBox(height: 24.h), + "Personal Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + height: 170.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 30.w, height: 30.h), + "Habib Wallet".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + ], + ), + Spacer(), + Consumer(builder: (context, habibWalletVM, child) { + return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, isExpanded: false) + .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); + }), + Spacer(), + CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 16.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: "Recharge".needTranslation, + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 12.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ), + ], + ).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + }), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 170.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 30.w, height: 30.h), + "Medical Files".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + ], + ), + Spacer(), + Wrap( + spacing: -8.h, + // runSpacing: 0.h, + children: [ + Utils.buildImgWithAssets( + icon: AppAssets.babyGirlImg, + height: 28.h, + width: 28.w, + border: 1, + fit: BoxFit.contain, + borderRadius: 50.r, + ), + Utils.buildImgWithAssets( + icon: AppAssets.femaleImg, + height: 28.h, + width: 28.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + Utils.buildImgWithAssets( + icon: AppAssets.maleImg, + height: 28.h, + width: 28.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + ], + ), + Spacer(), + CustomButton( + height: 40.h, + icon: AppAssets.add_icon, + iconSize: 16.w, + iconColor: AppColors.primaryRedColor, + textColor: AppColors.primaryRedColor, + text: "Add Member".needTranslation, + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 12.f, + onPressed: () { + DialogService dialogService = getIt.get(); + medicalFileViewModel.clearAuthValues(); + dialogService.showAddFamilyFileSheet( + label: "Add Family Member".needTranslation, + message: "Please fill the below field to add a new family member to your profile".needTranslation, + onVerificationPress: () { + medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); + }); + }, + ), + ], + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MedicalFilePage(), + ), + ); + }), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "Health Tools".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row + crossAxisSpacing: 12.w, + mainAxisSpacing: 18.h, + childAspectRatio: 0.8, + ), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: hmgHealthToolServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return ServiceGridViewItem( + hmgHealthToolServices[index], + index, + false, + isHealthToolIcon: true, + ); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "Support Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.virtual_tour_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Virtual Tour".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ), + ), + ).onPress(() { + Utils.openWebView( + url: 'https://hmgwebservices.com/vt_mobile/html/index.html', + ); + }), + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ), + ), + ), + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.latest_news_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Latest News".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ), + ), + ).onPress(() { + Utils.openWebView( + url: 'https://x.com/HMG', + ); + }), + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.hmg_contact_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "HMG Contact".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ), + ), + ).onPress(() { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.contactUs.tr(), + child: ContactUs(), + callBackFunc: () {}, + isFullScreen: false, + ); + }), + ) + ], + ) + ], + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), ], ), ), diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart new file mode 100644 index 0000000..3f0c44e --- /dev/null +++ b/lib/presentation/hmg_services/services_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class ServiceGridViewItem extends StatelessWidget { + final HmgServicesComponentModel hmgServiceComponentModel; + final int index; + final bool isHomePage; + final bool isLocked; + final bool isHealthToolIcon; + final Function? onTap; + + const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false, this.onTap, this.isHealthToolIcon = false}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + hmgServiceComponentModel.route != null + ? getIt.get().pushPageRoute(hmgServiceComponentModel.route!) + : hmgServiceComponentModel.onTap != null + ? hmgServiceComponentModel.onTap!() + : null; + }, + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 48.h, + width: 48.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: hmgServiceComponentModel.bgColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(12.h), + child: Utils.buildSvgWithAssets( + icon: hmgServiceComponentModel.icon, + iconColor: isHealthToolIcon ? null : AppColors.whiteColor, + fit: BoxFit.contain, + ), + ), + ), + SizedBox(height: 5.h), + hmgServiceComponentModel.title.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColor, + maxLine: 2, + ), + ], + )); + } +} diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index 31b4598..c693479 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -36,7 +35,7 @@ class LandingPageData { isBold: false, ), ServiceCardData( - serviceName: "health_calculators", + serviceName: "health_calculators_and_converts", icon: AppAssets.health_calculators_icon, title: "Health", subtitle: "Calculators", @@ -81,7 +80,7 @@ class LandingPageData { ServiceCardData( serviceName: "lab_results", icon: AppAssets.home_lab_result_icon, - title: "My Lab", + title: "Lab", subtitle: "Results", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, @@ -90,8 +89,8 @@ class LandingPageData { ), ServiceCardData( serviceName: "radiology_results", - icon: AppAssets.home_lab_result_icon, - title: "My Radiology", + icon: AppAssets.my_radiology_icon, + title: "Radiology", subtitle: "Results", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, @@ -101,8 +100,8 @@ class LandingPageData { ServiceCardData( serviceName: "prescriptions", icon: AppAssets.my_prescription_icon, - title: "My", - subtitle: "Prescriptions", + title: "Prescriptions", + subtitle: "Details", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, textColor: AppColors.blackColor, @@ -112,7 +111,7 @@ class LandingPageData { serviceName: "insurance_update", icon: AppAssets.insurance_update_icon, title: "Insurance", - subtitle: "Update", + subtitle: "Details", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, textColor: AppColors.blackColor, @@ -120,7 +119,7 @@ class LandingPageData { ), ServiceCardData( serviceName: "my_doctors", - icon: AppAssets.insurance_update_icon, + icon: AppAssets.my_doctors_icon, title: "My", subtitle: "Doctors", backgroundColor: AppColors.whiteColor, @@ -130,8 +129,8 @@ class LandingPageData { ), ServiceCardData( serviceName: "sick_leaves", - icon: AppAssets.insurance_update_icon, - title: "My Sick", + icon: AppAssets.my_sick_leave_icon, + title: "Sick", subtitle: "Leaves", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, @@ -142,44 +141,42 @@ class LandingPageData { static List getServiceCardsList = [ ServiceCardData( - icon: AppAssets.livecare_service, + serviceName: "livecare", + icon: AppAssets.small_livecare_icon, title: "LiveCare", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: Colors.transparent, - iconColor: Colors.transparent, - textColor: Colors.transparent, - isBold: true, - ), - ServiceCardData( - icon: AppAssets.lab_result_icon, - title: "Dermatology", - subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, + largeCardIcon: AppAssets.liveCareService, + backgroundColor: AppColors.successColor, + iconColor: AppColors.whiteColor, isBold: false, ), - ServiceCardData( - icon: AppAssets.my_prescription_icon, + // ServiceCardData( + // icon: AppAssets.homeBottom, + // title: "Dermatology", + // subtitle: "Explore our app, View our services and offers", + // largeCardIcon: AppAssets.homeBottom, + // backgroundColor: AppColors.primaryRedColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "home_health_care", + icon: AppAssets.homeBottom, title: "Home Health Care", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, + largeCardIcon: AppAssets.homeHealthCareService, + backgroundColor: AppColors.primaryRedColor, + iconColor: AppColors.whiteColor, isBold: false, ), ServiceCardData( - icon: AppAssets.insurance_update_icon, + serviceName: "pharmacy", + icon: AppAssets.pharmacy_icon, //359846 title: "Pharmacy", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, + largeCardIcon: AppAssets.pharmacyService, + backgroundColor: AppColors.pharmacyBGColor, + iconColor: null, + isBold: true, ), ]; } diff --git a/lib/presentation/home/data/service_card_data.dart b/lib/presentation/home/data/service_card_data.dart index 49e7e3d..3bc22d7 100644 --- a/lib/presentation/home/data/service_card_data.dart +++ b/lib/presentation/home/data/service_card_data.dart @@ -7,7 +7,7 @@ class ServiceCardData { final String icon; final String title; final String subtitle; - final Color iconColor; + final Color? iconColor; final Color textColor; final Color backgroundColor; final bool isBold; @@ -22,6 +22,6 @@ class ServiceCardData { this.backgroundColor = AppColors.whiteColor, this.iconColor = AppColors.blackColor, this.textColor = AppColors.blackColor, - this.isBold = false, + this.isBold = false }); } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 4046023..95884d4 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -9,25 +10,34 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_queue_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/authentication/quick_login.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart'; +import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart'; import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; @@ -35,11 +45,16 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/countdown_timer.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart'; import 'package:provider/provider.dart'; @@ -58,12 +73,13 @@ class _LandingPageState extends State { late AppState appState; late MyAppointmentsViewModel myAppointmentsViewModel; - late PrescriptionsViewModel prescriptionsViewModel; final CacheService cacheService = GetIt.instance(); - + late AppointmentRatingViewModel appointmentRatingViewModel; late InsuranceViewModel insuranceViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + late EmergencyServicesViewModel emergencyServicesViewModel; + late TodoSectionViewModel todoSectionViewModel; final SwiperController _controller = SwiperController(); @@ -71,10 +87,7 @@ class _LandingPageState extends State { void initState() { authVM = context.read(); habibWalletVM = context.read(); - // myAppointmentsViewModel = context.read(); - // prescriptionsViewModel = context.read(); - // insuranceViewModel = context.read(); - // immediateLiveCareViewModel = context.read(); + appointmentRatingViewModel = context.read(); authVM.savePushTokenToAppState(); if (mounted) { @@ -86,13 +99,29 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); - myAppointmentsViewModel.getPatientMyDoctors(); - prescriptionsViewModel.initPrescriptionsViewModel(); - insuranceViewModel.initInsuranceProvider(); + todoSectionViewModel.initializeTodoSectionViewModel(); immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); + emergencyServicesViewModel.checkPatientERAdvanceBalance(); + myAppointmentsViewModel.getPatientAppointmentQueueDetails(); + if(!appState.isRatedVisible) { + appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { + appointmentRatingViewModel.setClinicOrDoctor(false); + appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation); + openLastRating(); + appState.setRatedVisible(true); + }), + ); + } + }, + ); + } } }); super.initState(); @@ -102,9 +131,10 @@ class _LandingPageState extends State { Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); myAppointmentsViewModel = Provider.of(context, listen: false); - prescriptionsViewModel = Provider.of(context, listen: false); insuranceViewModel = Provider.of(context, listen: false); immediateLiveCareViewModel = Provider.of(context, listen: false); + emergencyServicesViewModel = Provider.of(context, listen: false); + todoSectionViewModel = Provider.of(context, listen: false); appState = getIt.get(); return PopScope( canPop: false, @@ -125,16 +155,25 @@ class _LandingPageState extends State { Navigator.of(context).push(springPageRoute(ProfileSettings())); }, name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), - imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, + imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, ).expanded : CustomButton( text: LocaleKeys.loginOrRegister.tr(context: context), onPressed: () async { await authVM.onLoginPressed(); + + // Navigator.pushReplacementNamed( + // // context, + // context, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("test123", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, @@ -154,25 +193,70 @@ class _LandingPageState extends State { ); }), Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: MedicalFilePage(), - // page: LoginScreen(), - ), - ); + // Navigator.of(context).push( + // CustomPageRoute( + // page: MedicalFilePage(), + // // page: LoginScreen(), + // ), + // ); }), Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 18.h, width: 18.h).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: MedicalFilePage(), - // page: LoginScreen(), - ), + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.contactUs.tr(), + child: ContactUs(), + callBackFunc: () {}, + isFullScreen: false, ); }), ], ), ], ).paddingSymmetrical(24.h, 0.h), + !appState.isAuthenticated + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + width: 50.w, + height: 60.h, + icon: AppAssets.symptomCheckerIcon, + fit: BoxFit.contain, + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "How are you feeling today?".needTranslation.toText14(isBold: true), + "Check your symptoms with this scale".needTranslation.toText12(fontWeight: FontWeight.w500), + SizedBox(height: 14.h), + CustomButton( + text: "Check your symptoms".needTranslation, + onPressed: () async { + context.navigateWithName(AppRoutes.userInfoSelection); + }, + backgroundColor: Color(0xFF2B353E), + borderColor: Color(0xFF2B353E), + textColor: AppColors.whiteColor, + fontSize: 14, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 40.h, + ), + ], + ) + ], + ), + ), + ).paddingSymmetrical(24.w, 0.h) + : SizedBox.shrink(), appState.isAuthenticated ? Column( children: [ @@ -193,8 +277,8 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); }), SizedBox(height: 16.h), - Consumer( - builder: (context, myAppointmentsVM, child) { + Consumer3( + builder: (context, myAppointmentsVM, immediateLiveCareVM, todoSectionVM, child) { return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -238,46 +322,232 @@ class _LandingPageState extends State { indicatorLayout: PageIndicatorLayout.COLOR, axisDirection: AxisDirection.right, controller: _controller, - itemHeight: 210 + 25, - pagination: const SwiperPagination( + itemHeight: 255.h, + pagination: SwiperPagination( alignment: Alignment.bottomCenter, - margin: EdgeInsets.only(top: 210 + 8 + 24), + margin: EdgeInsets.only(top: 240.h + 8 + 24), builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), ), itemBuilder: (BuildContext context, int index) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: true, - ), - ); + return (myAppointmentsVM.isPatientHasQueueAppointment && index == 0) + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + side: BorderSide(color: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), width: 2.w), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: myAppointmentsVM.currentQueueStatus == 0 ? "In Queue".needTranslation : "Your Turn".needTranslation, + backgroundColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.20), + textColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + ], + ), + SizedBox(height: 8.h), + "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + SizedBox(height: 2.h), + "Thank you for your patience, here is your queue number." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 8.h), + myAppointmentsVM.currentPatientQueueDetails.queueNo!.toText28(isBold: true), + SizedBox(height: 6.h), + myAppointmentsVM.patientQueueDetailsList.isNotEmpty ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + "Serving Now: ".needTranslation.toText14(isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + myAppointmentsVM.patientQueueDetailsList.first.queueNo!.toText12(isBold: true), + SizedBox(width: 8.w), + AppCustomChipWidget( + deleteIcon: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppAssets.call_for_vitals : AppAssets.call_for_doctor, + labelText: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? "Call for vital signs".needTranslation : "Call for Doctor".needTranslation, + iconColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, + textColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, + iconSize: 14.w, + backgroundColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), + ), + ], + ), + ], + ) : SizedBox(height: 12.h), + SizedBox(height: 5.h), + CustomButton( + text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), + onPressed: () {}, + backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), + borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), + textColor: Utils.getCardButtonTextColor(myAppointmentsVM.currentQueueStatus), + fontSize: 12.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ], + ), + ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentQueuePage(), + ), + ); + }) + : (immediateLiveCareVM.patientHasPendingLiveCareRequest && index == 0) + ? Column( + children: [ + SizedBox(height: 12.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: true, + side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Immediate LiveCare Request".needTranslation.toText16(isBold: true), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + AppCustomChipWidget( + labelText: immediateLiveCareVM.patientLiveCareHistoryList[0].stringCallStatus, + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), + textColor: AppColors.alertColor, + ), + SizedBox(width: 8.w), + AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate(immediateLiveCareVM.patientLiveCareHistoryList[0].arrivalTime), false)), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + // Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.cover), + ], + ), + SizedBox(height: 10.h), + "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + SizedBox(height: 8.h), + "Your turn is after ${immediateLiveCareVM.patientLiveCareHistoryList[0].patCount} patients.".needTranslation.toText14(isBold: true), + SizedBox(height: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Expected waiting time: ".needTranslation.toText12(isBold: true), + SizedBox(height: 7.h), + ValueListenableBuilder( + valueListenable: immediateLiveCareVM.durationNotifier, + builder: (context, duration, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildTime(duration), + ], + ); + }, + ), + ], + ), + // CustomButton( + // text: "View Details".needTranslation, + // onPressed: () async { + // Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); + // }, + // backgroundColor: Color(0xffFEE9EA), + // borderColor: Color(0xffFEE9EA), + // textColor: Color(0xffED1C2B), + // fontSize: 14.f, + // fontWeight: FontWeight.w500, + // borderRadius: 12.r, + // padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + // height: 40.h, + // ), + ], + ), + ), + ).paddingSymmetrical(0.h, 0.h).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); + }), + SizedBox(height: 12.h), + ], + ) + : (todoSectionVM.patientAncillaryOrdersList.isNotEmpty && index == 1) + ? AncillaryOrderCard( + order: todoSectionVM.patientAncillaryOrdersList.first, + isLoading: false, + isOrdersList: false, + onCheckIn: () { + log("Check-in for order: ${todoSectionVM.patientAncillaryOrdersList.first.orderNo}"); + }, + onViewDetails: () { + Navigator.of(context).push( + CustomPageRoute( + page: AncillaryOrderDetailsList( + appointmentNoVida: todoSectionVM.patientAncillaryOrdersList.first.appointmentNo ?? 0, + orderNo: todoSectionVM.patientAncillaryOrdersList.first.orderNo ?? 0, + projectID: todoSectionVM.patientAncillaryOrdersList.first.projectID ?? 0, + projectName: todoSectionVM.patientAncillaryOrdersList.first.projectName ?? "", + ), + ), + ); + }, + ) + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: + myAppointmentsVM.patientAppointmentsHistoryList[immediateLiveCareViewModel.patientHasPendingLiveCareRequest ? --index : index], + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: true, + ), + ); }, ) : Container( width: double.infinity, - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Padding( padding: EdgeInsets.all(12.h), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), SizedBox(height: 12.h), - "You do not have any upcoming appointment. Please book an appointment" - .needTranslation - .toText12(isCenter: true), + "You do not have any upcoming appointment. Please book an appointment".needTranslation.toText12(isCenter: true), SizedBox(height: 12.h), CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () { + getIt.get().onTabChanged(0); Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); }, backgroundColor: Color(0xffFEE9EA), @@ -288,7 +558,7 @@ class _LandingPageState extends State { padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), icon: AppAssets.add_icon, iconColor: AppColors.primaryRedColor, - height: 46.h, + height: 40.h, ), ], ), @@ -296,18 +566,20 @@ class _LandingPageState extends State { ).paddingSymmetrical(24.h, 0.h); }, ), - Consumer( - builder: (context, immediateLiveCareVM, child) { - return immediateLiveCareVM.patientHasPendingLiveCareRequest + + // Consumer for ER Online Check-In pending request + Consumer( + builder: (context, emergencyServicesVM, child) { + return emergencyServicesVM.patientHasAdvanceERBalance ? Column( children: [ - SizedBox(height: 12.h), + SizedBox(height: 4.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: true, - side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + side: BorderSide(color: AppColors.primaryRedColor, width: 3.h), ), width: double.infinity, child: Padding( @@ -319,19 +591,18 @@ class _LandingPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget( - labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus, - backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), - textColor: AppColors.alertColor, + labelText: "ER Online Check-In Request", + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), + textColor: AppColors.primaryRedColor, ), - Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), - // Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.contain), + Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), ], ), SizedBox(height: 8.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "You have a pending LiveCare request".needTranslation.toText12(isBold: true), + "You have ER Online Check-In Request".needTranslation.toText12(isBold: true), Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, @@ -345,7 +616,8 @@ class _LandingPageState extends State { ), ), ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); + Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); + // context.read().navigateToEROnlineCheckIn(); }), SizedBox(height: 12.h), ], @@ -353,6 +625,7 @@ class _LandingPageState extends State { : SizedBox(height: 12.h); }, ), + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -370,7 +643,7 @@ class _LandingPageState extends State { }), SizedBox(height: 16.h), Container( - height: 120.h, + height: 121.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: Column( children: [ @@ -391,7 +664,7 @@ class _LandingPageState extends State { icon: LandingPageData.getLoggedInServiceCardsList[index].icon, title: LandingPageData.getLoggedInServiceCardsList[index].title, subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor, + iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor!, textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, @@ -432,7 +705,7 @@ class _LandingPageState extends State { icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, title: LandingPageData.getNotLoggedInServiceCardsList[index].title, subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor, + iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor!, textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, @@ -457,9 +730,11 @@ class _LandingPageState extends State { SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), ], - ), + ).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ServicesPage())); + }), ], - ).paddingSymmetrical(24.h, 0.h), + ).paddingSymmetrical(24.w, 0.h), SizedBox( height: 340.h, child: ListView.separated( @@ -475,6 +750,7 @@ class _LandingPageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: LargeServiceCard( + serviceCardData: LandingPageData.getServiceCardsList[index], image: LandingPageData.getServiceCardsList[index].icon, title: LandingPageData.getServiceCardsList[index].title, subtitle: LandingPageData.getServiceCardsList[index].subtitle, @@ -484,7 +760,7 @@ class _LandingPageState extends State { ), ); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.w), + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), ), appState.isAuthenticated ? HabibWalletCard() : SizedBox(), @@ -498,19 +774,21 @@ class _LandingPageState extends State { void showQuickLogin(BuildContext context) { showCommonBottomSheetWithoutHeight( context, - title: "", + // title: "", isCloseButtonVisible: false, - child: StatefulBuilder( builder: (context, setState) { return QuickLogin( isDone: isDone, onPressed: () { // sharedPref.setBool(HAS_ENABLED_QUICK_LOGIN, true); - authVM.loginWithFingerPrintFace(() { + authVM.loginWithFingerPrintFace(() async { isDone = true; cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: true); setState(() {}); + await Future.delayed(Duration(milliseconds: 2000)).then((value) { + if (mounted) Navigator.pop(context); + }); }); }, ); @@ -524,4 +802,31 @@ class _LandingPageState extends State { }, ); } + + openLastRating() { + showCommonBottomSheetWithoutHeight( + context, + titleWidget: Selector( + selector: (_, vm) => vm.title, + builder: (context, title, child) { + final displayTitle = title ?? ''; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + displayTitle.toText20(weight: FontWeight.w600), + (context.select((vm) => vm.subTitle) ?? '').toText12(), + ], + ); + }, + ), + isCloseButtonVisible: true, + child: StatefulBuilder( + builder: (context, setState) { + + return RateAppointmentDoctor(); + }, + ), + isFullScreen: false, + ); + } } diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index cd95613..43cc3b9 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -1,11 +1,18 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart'; import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class LandingNavigation extends StatefulWidget { const LandingNavigation({super.key}); @@ -16,28 +23,48 @@ class LandingNavigation extends StatefulWidget { class _LandingNavigationState extends State { int _currentIndex = 0; - late AppState appState; final PageController _pageController = PageController(); @override Widget build(BuildContext context) { - appState = getIt.get(); + AppState appState = getIt.get(); return Scaffold( body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), children: [ const LandingPage(), - appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ const LandingPage(), - BookAppointmentPage(), - const LandingPage(), - appState.isAuthenticated ? /* need add news page */ ServicesPage() : const LandingPage(), + appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ FeedbackPage(), + SizedBox(), + // const ToDoPage(), + // appState.isAuthenticated ? UserInfoSelectionScreen() : /* need add news page */ SizedBox(), + SizedBox(), + ServicesPage(), ], ), bottomNavigationBar: BottomNavigation( currentIndex: _currentIndex, onTap: (index) { setState(() => _currentIndex = index); + if (_currentIndex == 2) { + getIt.get().onTabChanged(0); + context.navigateWithName(AppRoutes.bookAppointmentPage); + return; + } + if (_currentIndex == 3) { + if (appState.isAuthenticated) { + Navigator.of(context).push( + CustomPageRoute( + page: UserInfoSelectionScreen(), + ), + ); + } else { + Utils.openWebView( + url: 'https://x.com/HMG', + ); + } + return; + } _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); }, ), diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 6ea4507..b2649f9 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page. import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class HabibWalletCard extends StatelessWidget { diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index 3989523..5274d5b 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -1,16 +1,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../core/utils/utils.dart'; import '../../../theme/colors.dart'; class LargeServiceCard extends StatelessWidget { + final ServiceCardData serviceCardData; final String image; final String icon; final String title; @@ -18,6 +27,7 @@ class LargeServiceCard extends StatelessWidget { const LargeServiceCard({ super.key, + required this.serviceCardData, this.image = "", this.icon = "", this.title = "", @@ -27,37 +37,112 @@ class LargeServiceCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: 150.w, - padding: EdgeInsets.symmetric(horizontal: 3.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.transparent, borderRadius: 16.r), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + height: 350.h, + width: 230.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.transparent, borderRadius: 24.r), + child: Stack( children: [ - Image.asset(AppAssets.livecare_service, width: 220.w, fit: BoxFit.contain), - SizedBox(height: 10.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: icon, width: 24.w, height: 24.h), - Flexible(child: title.toText14(color: AppColors.blackColor, isBold: true, textOverflow: TextOverflow.clip, maxlines: 1)), - ], + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Image.asset( + serviceCardData.largeCardIcon, + fit: BoxFit.cover, + ), ), - subtitle.toText11(color: AppColors.blackColor), - SizedBox(height: 10.h), - CustomButton( - text: LocaleKeys.bookNow.tr(context: context), - onPressed: () {}, - backgroundColor: AppColors.borderOnlyColor, - borderColor: AppColors.borderOnlyColor, - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.bold, - borderRadius: 12.r, - padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), - height: 40.h, + Positioned( + bottom: 0.0, // Positions the child 0 logical pixels from the bottom + left: 0.0, + right: 0.0, + child: Container( + height: 180.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + customBorder: BorderRadius.only( + bottomLeft: Radius.circular(24.r), + bottomRight: Radius.circular(24.r), + ), + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 48.h, + width: 48.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: serviceCardData.backgroundColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(12.h), + child: Utils.buildSvgWithAssets( + icon: serviceCardData.icon, + iconColor: serviceCardData.iconColor, + fit: BoxFit.contain, + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + serviceCardData.title.toText14(isBold: true, color: AppColors.textColor), + SizedBox(width: 6.h), + serviceCardData.subtitle.toText14(weight: FontWeight.w500, color: AppColors.textColorLight), + ], + ), + ), + ], + ).paddingSymmetrical(16.w, 20.h), + CustomButton( + text: serviceCardData.isBold ? "Visit Pharmacy Online".needTranslation : LocaleKeys.bookNow.tr(context: context), + onPressed: () { + handleOnTap(); + }, + backgroundColor: serviceCardData.isBold ? AppColors.successLightColor.withValues(alpha: 0.2) : AppColors.bgRedLightColor, + borderColor: serviceCardData.isBold ? AppColors.successLightColor.withValues(alpha: 0.01) : AppColors.bgRedLightColor, + textColor: serviceCardData.isBold ? AppColors.successColor : AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + height: 40.h, + ).paddingSymmetrical(16.w, 0.h), + ], + ), + ), ), ], ), ); } + + void handleOnTap() { + switch (serviceCardData.serviceName) { + case "livecare": + { + getIt.get().onTabChanged(1); + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BookAppointmentPage(), + ), + ); + } + case "home_health_care": + { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: HhcProceduresPage(), + ), + ); + } + case "pharmacy": + { + Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + } } diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index af80e2d..6216880 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -15,7 +15,6 @@ import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import '../../../core/utils/utils.dart'; @@ -116,6 +115,7 @@ class SmallServiceCard extends StatelessWidget { case "emergency": context.read().flushData(); context.read().getTransportationOrders(showLoader: false,); + context.read().getRRTOrders(showLoader: false,); Navigator.of(context).push( CustomPageRoute( page: EmergencyServicesPage(), diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index f710ab2..8ef0697 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.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/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/home_health_care/hhc_order_detail_page.dart b/lib/presentation/home_health_care/hhc_order_detail_page.dart new file mode 100644 index 0000000..92c93d1 --- /dev/null +++ b/lib/presentation/home_health_care/hhc_order_detail_page.dart @@ -0,0 +1,246 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.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/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class HhcOrderDetailPage extends StatefulWidget { + const HhcOrderDetailPage({super.key}); + + @override + State createState() => _HhcOrderDetailPageState(); +} + +class _HhcOrderDetailPageState extends State { + @override + void initState() { + super.initState(); + final hmgServicesViewModel = context.read(); + scheduleMicrotask(() async { + await hmgServicesViewModel.getAllHhcOrders(); + }); + } + + Color _getStatusColor(int? statusId) { + switch (statusId) { + case 1: // Pending + return const Color(0xffCC9B14); + case 2: // Processing + return const Color(0xff2E303A); + case 3: // Completed + return const Color(0xff359846); + case 4: // Cancelled + case 6: // Rejected + case 7: // Rejected + return const Color(0xffD02127); + default: + return AppColors.greyColor; + } + } + + String _formatDate(String? dateString) { + if (dateString == null) return ''; + try { + final date = DateTime.parse(dateString); + return DateFormat('MMM dd, yyyy').format(date); + } catch (e) { + return dateString; + } + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return _buildOrderCard(GetCMCAllOrdersResponseModel(), isLoading: true); + }, + ); + } + + Widget _buildOrderCard(GetCMCAllOrdersResponseModel order, {bool isLoading = false}) { + final statusColor = _getStatusColor(order.statusId); + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and Date Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: (isLoading ? "Processing" : order.statusText ?? '') + .toText12( + color: statusColor, + fontWeight: FontWeight.w600, + ) + .toShimmer2(isShow: isLoading), + ), + SizedBox(width: 8.w), + (isLoading ? "Jan 15, 2024" : _formatDate(order.created)) + .toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w500, + ) + .toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 16.h), + + // Request ID + Row( + children: [ + if (!isLoading) ...[ + "Request ID:".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ], + (isLoading ? "12345" : "${order.iD ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Chips for Hospital, Service, and Amount + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + // Service + if (order.serviceText != null || isLoading) + AppCustomChipWidget( + icon: AppAssets.servicesBottom, + labelText: isLoading ? "Service Name" : order.serviceText ?? '-', + ).toShimmer2(isShow: isLoading), + ], + ), + + // // Cancel Button + // if (canCancel || isLoading) ...[ + // SizedBox(height: 16.h), + // Row( + // children: [ + // Expanded( + // child: CustomButton( + // text: "Cancel Order".needTranslation, + // onPressed: isLoading ? () {} : () => HhcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // fontSize: 14.f, + // fontWeight: FontWeight.w600, + // borderRadius: 10.r, + // height: 44.h, + // ).toShimmer2(isShow: isLoading), + // ), + // ], + // ), + // ] + ], + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any Home Health Care orders yet.".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "HHC Orders".needTranslation, + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, viewModel, child) { + if (viewModel.isHhcOrdersLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.hhcOrdersList.isEmpty) { + return _buildEmptyState(); + } + + return ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.hhcOrdersList.length, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final order = viewModel.hhcOrdersList.reversed.toList()[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: _buildOrderCard(order), + ), + ), + ); + }, + ); + }, + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/home_health_care/hhc_procedures_page.dart b/lib/presentation/home_health_care/hhc_procedures_page.dart new file mode 100644 index 0000000..be97a88 --- /dev/null +++ b/lib/presentation/home_health_care/hhc_procedures_page.dart @@ -0,0 +1,508 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_order_detail_page.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_selection_review_page.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/widgets/hhc_ui_selection_helper.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class HhcProceduresPage extends StatefulWidget { + const HhcProceduresPage({super.key}); + + @override + State createState() => _HhcProceduresPageState(); +} + +class _HhcProceduresPageState extends State { + @override + void initState() { + super.initState(); + final HmgServicesViewModel hmgServicesViewModel = context.read(); + final AppState appState = getIt.get(); + + scheduleMicrotask(() async { + final user = appState.getAuthenticatedUser(); + if (user != null) { + // Clear previous selections when entering the page + hmgServicesViewModel.clearHhcServicesSelection(); + await hmgServicesViewModel.getAllHhcOrders(); + await hmgServicesViewModel.getAllHhcServices(patientID: user.patientId ?? 0); + } + }); + } + + GetCMCAllOrdersResponseModel? _getPendingOrder(List orders) { + if (orders.isEmpty) return null; + + // Find pending or processing orders (status 1 or 2) + for (var order in orders) { + if (order.statusId == 1 || order.statusId == 2) { + return order; + } + } + + return null; + } + + Widget _buildPendingOrderCard(GetCMCAllOrdersResponseModel order) { + final canCancel = order.statusId == 1 || order.statusId == 2; + final isArabic = getIt.get().isArabic(); + + List selectedServices = []; + if (order.orderselectedservice != null) { + if (order.orderselectedservice is List) { + selectedServices = order.orderselectedservice as List; + } + } + + return Container( + width: double.infinity, + margin: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and Date Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Request ID:".needTranslation.toText14(color: AppColors.textColorLight, weight: FontWeight.w500), + SizedBox(width: 4.w), + "${order.iD ?? '-'}".toText16(isBold: true), + ], + ), + SizedBox(width: 8.w), + if (order.created != null) + DateFormat('MMM dd, yyyy').format(DateTime.parse(order.created!)).toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w500, + ), + ], + ), + SizedBox(height: 8.h), + + // Services List + if (selectedServices.isNotEmpty) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: AppColors.greyColor.withAlpha(51), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.medical_services_outlined, + size: 18.h, + color: AppColors.primaryRedColor, + ), + SizedBox(width: 6.w), + "Requested Services".needTranslation.toText14( + weight: FontWeight.w600, + color: AppColors.blackColor, + ), + ], + ), + SizedBox(height: 8.h), + ...selectedServices.asMap().entries.map((entry) { + int index = entry.key; + var service = entry.value; + String serviceName = ''; + + if (service is Map) { + if (isArabic) { + serviceName = service['SelectedServiceNameAR']?.toString() ?? + service['selectedServiceNameAR']?.toString() ?? + service['SelectedServiceName']?.toString() ?? + service['selectedServiceName']?.toString() ?? + ''; + } else { + serviceName = service['SelectedServiceName']?.toString() ?? service['selectedServiceName']?.toString() ?? ''; + } + } + + return Padding( + padding: EdgeInsets.only(top: index > 0 ? 6.h : 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(top: 6.h), + width: 6.w, + height: 6.w, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 8.w), + Expanded( + child: serviceName.toText14( + color: AppColors.blackColor, + weight: FontWeight.w500, + maxlines: 3, + ), + ), + ], + ), + ); + }), + ], + ), + ), + ], + + SizedBox(height: 8.h), + + // Info message + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: AppColors.infoBannerBgColor, + borderRadius: BorderRadius.circular(10.r), + border: Border.all( + color: AppColors.infoBannerBorderColor, + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 20.w, + color: AppColors.infoBannerIconColor, + ), + SizedBox(width: 8.w), + Expanded( + child: "You have a pending order. Please wait for it to be processed.".needTranslation.toText12( + color: AppColors.infoBannerTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + if (canCancel) ...[ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Cancel Order".needTranslation, + onPressed: () => HhcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 10.r, + height: 44.h, + ), + ), + ], + ), + ] + ], + ), + ), + ); + } + + _buildServicesListBottomsSheet(List services) { + showCommonBottomSheetWithoutHeight( + hasBottomPadding: false, + padding: EdgeInsets.only(top: 24.h), + context, + title: 'Select Services'.needTranslation, + isCloseButtonVisible: true, + isDismissible: true, + callBackFunc: () {}, + child: services.isEmpty + ? Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Text( + 'No services available'.needTranslation, + style: TextStyle( + fontSize: 16.h, + color: AppColors.greyTextColor, + ), + ), + ), + ) + : Consumer( + builder: (context, hmgServicesViewModel, child) { + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + final hasSelectedServices = pendingOrder == null && hmgServicesViewModel.selectedHhcServices.isNotEmpty; + + return Stack( + children: [ + // Services List + ConstrainedBox( + constraints: BoxConstraints(maxHeight: SizeUtils.height * 0.7), + child: ListView.separated( + separatorBuilder: (context, index) => Divider(color: AppColors.dividerColor).paddingSymmetrical(16.w, 0), + padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: hasSelectedServices ? 160.h : 8.h), + shrinkWrap: true, + itemCount: services.length, + itemBuilder: (context, index) { + final service = services[index]; + final isSelected = hmgServicesViewModel.isHhcServiceSelected(service); + final isArabic = getIt.get().isArabic(); + final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: InkWell( + onTap: () => hmgServicesViewModel.toggleHhcServiceSelection(service), + borderRadius: BorderRadius.circular(16.r), + child: Container( + padding: EdgeInsets.all(8.w), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(5.r), + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : AppColors.borderGrayColor, + width: 1.w, + ), + ), + child: isSelected ? Icon(Icons.check, size: 18.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + serviceName.toText16( + weight: FontWeight.w500, + color: AppColors.blackColor, + maxlines: 2, + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + + if (hasSelectedServices) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Selected Services".needTranslation.toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w600, + ), + "${hmgServicesViewModel.selectedHhcServices.length} service(s) selected".toText14( + isBold: true, + weight: FontWeight.bold, + ), + ], + ), + ], + ), + SizedBox(height: 16.h), + CustomButton( + borderWidth: 0, + text: "Next".needTranslation, + onPressed: () { + Navigator.pop(context); + _proceedWithSelectedService(); + }, + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + ), + ], + ), + ), + ), + ], + ); + }, + ), + ); + } + + void _proceedWithSelectedService() async { + final navigationServices = getIt.get(); + final appState = getIt.get(); + final hmgServicesViewModel = context.read(); + if (hmgServicesViewModel.selectedHhcServices.isNotEmpty) { + hmgServicesViewModel.setSelectedServiceForHhcOrder(hmgServicesViewModel.selectedHhcServices.first); + + bool result = await navigationServices.push( + CustomPageRoute( + page: MapUtilityScreen( + confirmButtonString: "Submit Request ".needTranslation, + titleString: "Select Location", + subTitleString: "Please select the location".needTranslation, + isGmsAvailable: appState.isGMSAvailable, + ), + direction: AxisDirection.down), + ); + + if (!result) { + return; + } + + Navigator.of(context).push( + CustomPageRoute( + page: HhcSelectionReviewPage(selectedServices: hmgServicesViewModel.selectedHhcServices), + direction: AxisDirection.left, + ), + ); + } + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(16.w), + itemCount: 10, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Home Health Care".needTranslation, + history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)), + bottomChild: Consumer( + builder: (BuildContext context, HmgServicesViewModel hmgServicesViewModel, Widget? child) { + if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) { + return SizedBox.shrink(); + } + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + if (pendingOrder == null) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + borderWidth: 0, + text: "Create new request".needTranslation, + onPressed: () => _buildServicesListBottomsSheet(hmgServicesViewModel.hhcServicesList), + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + } + + return SizedBox.shrink(); + }, + ), + child: Consumer( + builder: (context, hmgServicesViewModel, child) { + if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) { + return _buildLoadingShimmer(); + } + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + if (pendingOrder != null) { + return _buildPendingOrderCard(pendingOrder); + } else { + return Column( + children: [ + Center( + child: Utils.getNoDataWidget( + context, + noDataText: "You have no pending requests.".needTranslation, + ), + ), + ], + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/presentation/home_health_care/hhc_selection_review_page.dart b/lib/presentation/home_health_care/hhc_selection_review_page.dart new file mode 100644 index 0000000..37410e2 --- /dev/null +++ b/lib/presentation/home_health_care/hhc_selection_review_page.dart @@ -0,0 +1,246 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/map/location_map_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + +class HhcSelectionReviewPage extends StatefulWidget { + final List selectedServices; + + const HhcSelectionReviewPage({super.key, required this.selectedServices}); + + @override + State createState() => _HhcSelectionReviewPageState(); +} + +class _HhcSelectionReviewPageState extends State { + @override + void initState() { + super.initState(); + // Initialize ViewModel state with selected services + WidgetsBinding.instance.addPostFrameCallback((_) { + final hmgServicesViewModel = context.read(); + if (widget.selectedServices.isNotEmpty) { + hmgServicesViewModel.setSelectedServiceForHhcOrder(widget.selectedServices.first); + } + }); + } + + @override + Widget build(BuildContext context) { + final appState = getIt.get(); + final isArabic = appState.isArabic(); + + return CollapsingListView( + title: "Summary".needTranslation, + bottomChild: _buildBottomButton(), + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSelectedServicesCard(isArabic), + SizedBox(height: 16.h), + _buildLocationMap(), + SizedBox(height: 16.h), + ], + ), + ), + ); + } + + Widget _buildSelectedServicesCard(bool isArabic) { + return Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Selected Services".needTranslation.toText14( + weight: FontWeight.w600, + color: AppColors.textColor, + letterSpacing: -0.4, + ), + SizedBox(height: 12.h), + Wrap( + spacing: 8.w, + runSpacing: 12.w, + children: widget.selectedServices.map((service) { + final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); + return AppCustomChipWidget(labelText: serviceName.needTranslation); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildLocationMap() { + final locationViewModel = getIt.get(); + final geocodeResponse = locationViewModel.geocodeResponse; + final mapCapturedLocation = locationViewModel.mapCapturedLocation; + + // Check if we have location data + if (mapCapturedLocation == null || (mapCapturedLocation.lat == 0.0 && mapCapturedLocation.lng == 0.0)) { + return SizedBox.shrink(); + } + + final double lat = mapCapturedLocation?.lat ?? 0.0; + final double lng = mapCapturedLocation?.lng ?? 0.0; + + if (lat == 0.0 || lng == 0.0) return SizedBox.shrink(); + + // Get address from geocode response + String address = "Selected Location".needTranslation; + if (geocodeResponse != null && geocodeResponse.results.isNotEmpty) { + address = geocodeResponse.results.first.formattedAddress; + } + + return LocationMapWidget( + latitude: lat, + longitude: lng, + address: address, + onDirectionsTap: () => _launchDirectionsToLocation(lat, lng, address), + ); + } + + Widget _buildBottomButton() { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: CustomButton( + borderWidth: 0, + text: "Confirm".needTranslation, + onPressed: () => _handleConfirm(), + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + ), + ); + } + + void _launchDirectionsToLocation(double lat, double lng, String locationName) { + if (lat != 0.0 && lng != 0.0) { + MapsLauncher.launchCoordinates(lat, lng, locationName); + } + } + + showSuccessBottomSheet(int requestId, HmgServicesViewModel hmgServicesViewModel) { + return showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: "Your request has been successfully submitted.".needTranslation), + Row( + children: [ + "Here is your request #: ".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ("$requestId").toText16(isBold: true), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + context.pop(); + context.pop(); + hmgServicesViewModel.getAllHhcOrders(); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + } + + void _handleConfirm() { + final hmgServicesViewModel = context.read(); + final appState = getIt.get(); + + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to submit this request?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + // Create the services list from all selected services + final servicesList = widget.selectedServices.map((selectedService) { + return PatientERCMCInsertServicesList( + recordID: selectedService.iD, + serviceID: selectedService.serviceID, + selectedServiceName: selectedService.text, + selectedServiceNameAR: selectedService.textN, + price: selectedService.price, + vAT: selectedService.priceVAT, + totalPrice: selectedService.priceTotal, + ); + }).toList(); + + // For HHC, we don't need hospital selection, use a default projectID or 0 + await hmgServicesViewModel.addHhcOrder( + projectID: 0, + // HHC doesn't require hospital/project selection + orderServiceID: widget.selectedServices.first.orderServiceID ?? 4, + // HHC service ID + services: servicesList, + onSuccess: (requestId) { + LoaderBottomSheet.hideLoader(); + showSuccessBottomSheet(requestId, hmgServicesViewModel); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + }, + ); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } +} diff --git a/lib/presentation/home_health_care/widgets/hhc_ui_selection_helper.dart b/lib/presentation/home_health_care/widgets/hhc_ui_selection_helper.dart new file mode 100644 index 0000000..688612c --- /dev/null +++ b/lib/presentation/home_health_care/widgets/hhc_ui_selection_helper.dart @@ -0,0 +1,90 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/order_update_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class HhcUiSelectionHelper { + static void showCancelConfirmationDialog({ + required BuildContext context, + required GetCMCAllOrdersResponseModel order, + }) { + final HmgServicesViewModel hmgServicesViewModel = context.read(); + + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to cancel this order?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + final requestModel = OrderUpdateRequestModel( + presOrderID: order.iD, + rejectionReason: "Cancelled by user", + presOrderStatus: 4, // Cancelled status + editedBy: 3, + ); + + await hmgServicesViewModel.updateHhcPresOrder( + requestModel: requestModel, + onSuccess: (_) async { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: "Order has been cancelled successfully".needTranslation), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + context.pop(); + hmgServicesViewModel.getAllHhcOrders(); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + }, + ); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } +} diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 437c870..415d66f 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -118,7 +118,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { ), child: Padding( padding: EdgeInsets.all(16.h), - child: Column( + child: insuranceApprovalResponseModel.apporvalDetails != null ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "Approval Details".toText16(isBold: true), @@ -128,7 +128,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { children: [ LocaleKeys.procedureName.tr(context: context).toText14(isBold: true), Expanded( - child: insuranceApprovalResponseModel.apporvalDetails!.procedureName! + child: (insuranceApprovalResponseModel.apporvalDetails!.procedureName ?? "") .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, textAlignment: TextAlign.end, maxLine: 2), ), ], @@ -146,11 +146,11 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "${LocaleKeys.usageStatus.tr(context: context)}: ".toText14(isBold: true), - insuranceApprovalResponseModel.apporvalDetails!.isInvoicedDesc!.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + (insuranceApprovalResponseModel.apporvalDetails!.isInvoicedDesc ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), ], ), ], - ), + ) : SizedBox.shrink(), ), ) ], diff --git a/lib/presentation/insurance/insurance_home_page.dart b/lib/presentation/insurance/insurance_home_page.dart index bd195c3..b005e42 100644 --- a/lib/presentation/insurance/insurance_home_page.dart +++ b/lib/presentation/insurance/insurance_home_page.dart @@ -15,12 +15,10 @@ import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_upd import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; import 'widgets/insurance_history.dart'; @@ -74,7 +72,7 @@ class _InsuranceHomePageState extends State { padding: EdgeInsets.only(top: 24.h), child: PatientInsuranceCard( insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, - isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))), + isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))).paddingSymmetrical(24.w, 0.h), ) : Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.12), diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index bea2e56..ee31538 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -8,11 +8,8 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class InsuranceApprovalCard extends StatelessWidget { diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 1c7b1b9..341e234 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -12,7 +12,6 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; class InsuranceHistory extends StatelessWidget { @@ -68,7 +67,7 @@ class InsuranceHistory extends StatelessWidget { Row( children: [ CustomButton( - text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription!, + text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription ?? "", onPressed: () {}, backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), borderColor: AppColors.primaryRedColor.withOpacity(0.0), diff --git a/lib/presentation/insurance/widgets/insurance_update_details_card.dart b/lib/presentation/insurance/widgets/insurance_update_details_card.dart index acdf1c7..c3bbcd7 100644 --- a/lib/presentation/insurance/widgets/insurance_update_details_card.dart +++ b/lib/presentation/insurance/widgets/insurance_update_details_card.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -12,7 +11,6 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; class PatientInsuranceCardUpdateCard extends StatelessWidget { diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 5190e5b..fde5811 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -34,7 +34,7 @@ class PatientInsuranceCard extends StatelessWidget { width: double.infinity, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 24, + borderRadius: 24.r, ), child: Padding( padding: EdgeInsets.all(16.h), @@ -48,8 +48,9 @@ class PatientInsuranceCard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true), - "Policy: ${insuranceCardDetailsModel.insurancePolicyNo}".toText12(isBold: true, color: AppColors.lightGrayColor), + SizedBox( + width: MediaQuery.of(context).size.width * 0.45, child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true)), + "Policy: ${insuranceCardDetailsModel.insurancePolicyNo}".needTranslation.toText12(isBold: true, color: AppColors.lightGrayColor), ], ), AppCustomChipWidget( @@ -112,6 +113,6 @@ class PatientInsuranceCard extends StatelessWidget { ], ), ), - ).paddingSymmetrical(24.h, 0.h); + ).paddingSymmetrical(0.h, 0.h); } } diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart new file mode 100644 index 0000000..29ded69 --- /dev/null +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -0,0 +1,231 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart' show AnimationConfiguration, SlideAnimation, FadeInAnimation; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import 'lab_order_by_test.dart'; + +class AlphabeticScroll extends StatefulWidget{ + final List alpahbetsAvailable; + final List details; + final AppState appState; + final LabViewModel labViewModel; + final DateRangeSelectorRangeViewModel rangeViewModel; + + const AlphabeticScroll({super.key, required this.alpahbetsAvailable, required this.details, required this.appState, required this.labViewModel, required this.rangeViewModel}); + + @override + State createState() => _AlphabetScrollPageState(); +} + +class _AlphabetScrollPageState extends State { + final ItemScrollController itemScrollController = ItemScrollController(); + final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); + final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); + final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create(); + final ScrollController _scrollController = ScrollController(); + + Map> data = {}; + Map density = {}; + + Map _offsetMap = {}; + Map _offsetKeys = {}; + int _activeIndex = 0; // <-- Highlighted letter + + @override + void initState() { + super.initState(); + scheduleMicrotask((){ + for(var char in widget.alpahbetsAvailable){ + data[char] = widget.details.where((element)=>element.description?.toLowerCase().startsWith(char.toLowerCase()) == true).toList(); + + } + setState((){}); + + }); + itemPositionsListener.itemPositions.addListener((){ + + final positions = itemPositionsListener.itemPositions.value; + + if (positions.isEmpty) return; + + // Get FIRST visible item (top-most) + final firstVisible = positions + .where((p) => p.itemTrailingEdge > 0) // visible + .reduce((min, p) => + p.itemLeadingEdge < min.itemLeadingEdge ? p : min); + + if(_activeIndex == firstVisible.index) return ; + setState(() { + _activeIndex = firstVisible.index; + }); + + print("Active index = $_activeIndex"); + }); + } + + @override + void dispose() { + itemPositionsListener.itemPositions.removeListener((){ + + }); + super.dispose(); + + } + + void _scrollToLetter(String letter) async { + + // itemScrollController.jumpTo(index:density[letter]?.toInt()??0, ); + itemScrollController.scrollTo( + index: data.keys.toList().indexOf(letter).toInt()??0, + duration: Duration(seconds: 2), + curve: Curves.easeInOutCubic); + // final key = _offsetKeys[letter]; + // if (key == null) return; + // + // BuildContext? ctx = key.currentContext; + // + // // Retry until built (max 100ms) + // int retry = 0; + // while (ctx == null && retry < 5) { + // await Future.delayed(Duration(milliseconds: 20)); + // ctx = key.currentContext; + // retry++; + // } + // + // if (ctx == null) { + // print("❌ $letter still not built"); + // return; + // } + // + // final renderBox = ctx.findRenderObject() as RenderBox; + // final yOffset = renderBox.localToGlobal(Offset.zero).dy; + // + // _scrollController.animateTo( + // _scrollController.offset + yOffset - 80, + // duration: Duration(milliseconds: 400), + // curve: Curves.easeInOut, + // ); + } + + @override + Widget build(BuildContext context) { + + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, // Add this + children: [ + Expanded( + child: Column( + children: [ + SizedBox( + height: (MediaQuery.sizeOf(context).height), + child: _buildList(widget.alpahbetsAvailable) + ), + ], + ), + ), + + SizedBox( + width: 24.w, + height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h), + child: Column( + mainAxisSize: MainAxisSize.max, // Add this + mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start + crossAxisAlignment: CrossAxisAlignment.center, + children:List.generate(widget.alpahbetsAvailable.length, (i) { + final isActive = (i == _activeIndex); + + return GestureDetector( + onTap: () { + setState(() => _activeIndex = i); + _scrollToLetter(widget.alpahbetsAvailable[i]); + }, + child: TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), + duration: Duration(milliseconds: 120), + curve: Curves.easeOut, + builder: (_, scale, child) { + return Transform.scale( + scale: scale, + child: Opacity( + opacity: isActive ? 1.0 : 0.5, + child: widget.alpahbetsAvailable[i].toUpperCase().toText14( + color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor + ), + ), + ); + }, + ), + ); + } + ), + ), + ), + ], + ), + ); + // ); + } + + Widget _buildList(List alphabet) { + return ScrollablePositionedList.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemScrollController: itemScrollController, + scrollOffsetController: scrollOffsetController, + itemPositionsListener: itemPositionsListener, + scrollOffsetListener: scrollOffsetListener, + itemCount: data.length, + itemBuilder: (_, index) { + final letter = alphabet[index].toLowerCase(); + final items = data[letter]!; + return Container( + key: _offsetKeys[letter], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + List.generate(items.length,(itemIndex)=> + AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: LabOrderByTest( + + appState: getIt(), + onTap: () { + if (items[itemIndex].model != null) { + widget.rangeViewModel.flush(); + widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, + (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!),""); + } + }, + tests: items[itemIndex], + index: itemIndex, + isExpanded: true)), + ), + )) + // ...items.indexed((item) => + // + // + // + // ) + + ), + ); + }, + ); + } +} diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 2b791ae..837f482 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -12,7 +11,6 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; class LabOrderByTest extends StatelessWidget { final VoidCallback onTap; @@ -27,6 +25,7 @@ class LabOrderByTest extends StatelessWidget { @override build(BuildContext context) { return AnimatedContainer( + key:key, duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 0b9d093..90651f1 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingToolbar( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: Consumer( builder: (context, model, child) { return SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ CustomButton( text: LocaleKeys.byClinic.tr(context: context), onPressed: () { model.setIsSortByClinic(true); }, backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), SizedBox(width: 8.h), CustomButton( text: LocaleKeys.byHospital.tr(context: context), onPressed: () { model.setIsSortByClinic(false); }, backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available model.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (model.patientLabOrdersViewList.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.patientLabOrdersViewList.length, itemBuilder: (context, index) { final group = model.patientLabOrdersViewList[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder() .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Text( model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'), style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...group.map((order) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: ("Order No: ".needTranslation + order.orderNo!), ), AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), ), AppCustomChipWidget( labelText: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), ), ], ), // Row( // children: [ // CustomButton( // text: ("Order No: ".needTranslation + order.orderNo!), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), // SizedBox(height: 8.h), // Row( // children: [ // CustomButton( // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), SizedBox(height: 12.h), Row( children: [ Expanded(flex: 2, child: SizedBox()), // Expanded( // flex: 1, // child: Container( // height: 40.h, // width: 40.w, // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // color: AppColors.textColor, // borderRadius: 12, // ), // child: Padding( // padding: EdgeInsets.all(12.h), // child: Transform.flip( // flipX: _appState.isArabic(), // child: Utils.buildSvgWithAssets( // icon: AppAssets.forward_arrow_icon_small, // iconColor: AppColors.whiteColor, // fit: BoxFit.contain, // ), // ), // ), // ).onPress(() { // model.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }), // ) Expanded( flex:2, child: CustomButton( icon: AppAssets.view_report_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, text: "View Results".needTranslation, onPressed: () { model.currentlySelectedPatientOrder = order; labProvider.getPatientLabResultByHospital(order); labProvider.getPatientSpecialResult(order); Navigator.of(context).push( CustomPageRoute(page: LabResultByClinic(labOrder: order)), ); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ) ], ), SizedBox(height: 12.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), SizedBox(height: 12.h), ], ); }).toList(), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation)) : // By Test or other tabs keep existing behavior (model.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: model.indexedCharacterForUniqueTest, details: model.uniqueTestsList, labViewModel: model, rangeViewModel: rangeViewModel, appState: _appState, ) ], ) ); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_item_view.dart b/lib/presentation/lab/lab_result_item_view.dart index 9269c6d..db80918 100644 --- a/lib/presentation/lab/lab_result_item_view.dart +++ b/lib/presentation/lab/lab_result_item_view.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class LabResultItemView extends StatelessWidget { diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart index a603ecb..ad4a032 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.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/core/utils/utils.dart'; diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 8893ffd..3f05443 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -26,7 +26,7 @@ class LabResultList extends StatelessWidget { shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { var labItem = list[index]; return LabOrderResultItem(onTap: () { - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!); + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!, labItem.uOM ?? ""); }, tests: labItem, index: index, diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index 36cdc2b..c6841b3 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -2,19 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart' show LabViewModel; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:provider/provider.dart'; class LabOrderResultItem extends StatelessWidget { @@ -46,7 +41,7 @@ class LabOrderResultItem extends StatelessWidget { padding: EdgeInsets.only(bottom: 8.h), child: '${tests!.description}'.toText14(weight: FontWeight.w500), ), - '${tests!.packageShortDescription}'.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + (tests!.packageShortDescription ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 12.h), Row( mainAxisSize: MainAxisSize.max, @@ -58,22 +53,25 @@ class LabOrderResultItem extends StatelessWidget { fontSize: 24.f, fontWeight: FontWeight.w600, fontFamily: 'Poppins', - color: context.read().getColor( - tests?.calculatedResultFlag ?? "", - ), + color: tests!.checkIfGraphShouldBeDisplayed() + ? context.read().getColor( + tests?.calculatedResultFlag ?? "", + ) + : Colors.grey.shade700, letterSpacing: -2, ), ), ), SizedBox(width: 4.h,), Visibility( - visible: tests?.checkIfGraphShouldBeDisplayed() == true, + // visible: tests?.checkIfGraphShouldBeDisplayed() == true, + visible: true, child: Expanded( flex: 2, child: Visibility( visible: tests?.referanceRange != null, child: Text( - "(Reference range ${tests?.referanceRange})".needTranslation, + "(Reference range: ${tests?.referanceRange})".needTranslation, style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w500, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 2330d0d..1d54e06 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; @@ -59,115 +60,72 @@ class LabResultDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - recentLabResult.testCode ?? "", - style: TextStyle( - fontSize: 32, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.textColor, - letterSpacing: -2), - ), - Text( - "Result of ${recentLabResult.verifiedOn ?? ""}".needTranslation, - style: TextStyle( - fontSize: 12, - fontFamily: 'Poppins', - fontWeight: FontWeight.w500, - color: AppColors.greyTextColor, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + (recentLabResult.testCode ?? "").toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), + Visibility( + visible: recentLabResult.checkIfGraphShouldBeDisplayed() == true, + child: Column( + spacing: 6.h, + children: [ + context + .read() + .getSeverityText( + recentLabResult.calculatedResultFlag ?? "", + ) + .tr() + .toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: context.read().getColor( + recentLabResult.calculatedResultFlag ?? "", + )), + ], + ), + ), + ], ), + SizedBox(height: 4.h), + ("Result of ${recentLabResult.verifiedOn ?? ""}".needTranslation).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), ], ), - //todo change the text color according to the provided test values Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [ Expanded( child: Row( - mainAxisSize: MainAxisSize.max, children: [ Flexible( - child: Text( - "${recentLabResult.resultValue} ${recentLabResult.uOM}" ?? - "", - style: TextStyle( - fontSize: 24.f, - fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + child: ("${recentLabResult.resultValue} ${recentLabResult.uOM}" ?? "").toText28( + isBold: true, color: context.read().getColor( recentLabResult.calculatedResultFlag ?? "", - ), - letterSpacing: -2, - ), - overflow: TextOverflow.ellipsis, // prevent overflow - maxLines: 1, - softWrap: false, - ), + ), + letterSpacing: -2), ), SizedBox(width: 4.h,), - Expanded( - flex: 2, - child: Visibility( - visible: recentLabResult.referanceRange != null, - child: Text( - "(Reference range ${recentLabResult.referanceRange})".needTranslation, - style: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w500, - fontFamily: 'Poppins', - color: AppColors.greyTextColor, - ), - // overflow: TextOverflow.ellipsis, - // maxLines: 2, - softWrap: true, - ), - ), - ), - ], - ), - ), - Visibility( - visible: - recentLabResult.checkIfGraphShouldBeDisplayed() == true, - child: Column( - spacing: 6.h, - children: [ - context - .read() - .getSeverityText( - recentLabResult.calculatedResultFlag ?? "", - ) - .tr() - .toText10( - weight: FontWeight.w500, - color: AppColors.greyTextColor), - Utils.buildSvgWithAssets( - icon: AppAssets.lab_result_indicator, - width: 21, - height: 23, - iconColor: context.read().getColor( - recentLabResult.calculatedResultFlag ?? "", - )), ], ), ), - // SizedBox( - // width: 21, - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.lab_result_indicator, - // width: 21, - // height: 23, - // iconColor: context.read().getColor( - // recentLabResult.calculatedResultFlag ?? "", - // ), - // ), - // ), ], - ) - + ), + Visibility( + visible: recentLabResult.referanceRange != null, + child: Text( + "Reference range: \n${recentLabResult.referanceRange!.trim()}".needTranslation, + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ), ], )); @@ -279,29 +237,41 @@ class LabResultDetails extends StatelessWidget { dataPoints: labmodel.filteredGraphValues, makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, + showGridLines: true, leftLabelInterval: getInterval(labmodel), - maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, + // maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, + maxY: (labmodel.maxY), + minY: labmodel.minY, maxX: labmodel.filteredGraphValues.length.toDouble()-.75, + horizontalInterval: .1, + getDrawingHorizontalLine: (value){ + value = double.parse(value.toStringAsFixed(1)); + if(value == labmodel.highRefrenceValue ||value== labmodel.lowRefenceValue) { + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.6), + strokeWidth: 1, + // dashArray: [5, 5], + ); + } + return FlLine( + color: Colors.transparent, + strokeWidth: 1, + );; + }, leftLabelFormatter: (value) { - return leftLabels(value.toStringAsFixed(2)); - // switch (value.toInt()) { - // case 10: - // return leftLabels("Critical Low".needTranslation); - // case 30: - // return leftLabels("Low".needTranslation); - // case 50: - // return leftLabels("Normal".needTranslation); - // case 70: - // return leftLabels("High".needTranslation); - // case 90: - // return leftLabels( - // "Critical High".needTranslation); - // default: - // return SizedBox.shrink(); + value = double.parse(value.toStringAsFixed(1)); + // return leftLabels(value.toStringAsFixed(2)); + if(value == labmodel.highRefrenceValue) + return leftLabels("High".needTranslation); + + if(value== labmodel.lowRefenceValue) + return leftLabels("Low".needTranslation); + + return SizedBox.shrink(); // } }, - graphColor:graphColor , - graphShadowColor: graphColor.withOpacity(.1), + graphColor:AppColors.blackColor, + graphShadowColor: Colors.transparent, graphGridColor: graphColor.withOpacity(.4), bottomLabelFormatter: (value, data) { if(data.isEmpty) return SizedBox.shrink(); @@ -316,6 +286,9 @@ class LabResultDetails extends StatelessWidget { } return SizedBox.shrink(); }, + rangeAnnotations: RangeAnnotations( + horizontalRangeAnnotations: _buildRangeShades(labmodel) + ), minX:(labmodel.filteredGraphValues.length == 1)?null : -.2, scrollDirection: Axis.horizontal, height: 180.h); @@ -324,6 +297,29 @@ class LabResultDetails extends StatelessWidget { } } + List _buildRangeShades( LabViewModel model,) { + List ranges = []; + + ranges.add(HorizontalRangeAnnotation( + y1:model.minY, + y2: model.lowRefenceValue, + color: AppColors.highAndLow.withOpacity(0.05), + )); + + ranges.add(HorizontalRangeAnnotation( + y1:model.lowRefenceValue, + y2: model.highRefrenceValue, + color: AppColors.bgGreenColor.withOpacity(0.05), + )); + + ranges.add(HorizontalRangeAnnotation( + y1:model.highRefrenceValue, + y2: model.maxY, + color: AppColors.criticalLowAndHigh.withOpacity(0.05), + )); + return ranges; + } + Widget labHistoryList(LabViewModel labmodel) { return SizedBox( height: labmodel.filteredGraphValues.length<3?labmodel.filteredGraphValues.length*64:180.h, @@ -347,14 +343,15 @@ class LabResultDetails extends StatelessWidget { } double? getInterval(LabViewModel labmodel) { - var maxX = labmodel.maxY; - if(maxX<1) return .5; - if(maxX >1 && maxX < 5) return 1; - if(maxX >5 && maxX < 10) return 5; - if(maxX >10 && maxX < 50) return 10; - if(maxX >50 && maxX < 100) return 20; - if(maxX >100 && maxX < 200) return 30; - return 50; + return .1; + // var maxX = labmodel.maxY; + // if(maxX<1) return .5; + // if(maxX >1 && maxX < 5) return 1; + // if(maxX >5 && maxX < 10) return 5; + // if(maxX >10 && maxX < 50) return 10; + // if(maxX >50 && maxX < 100) return 20; + // if(maxX >100 && maxX < 200) return 30; + // return 50; } Widget getLabDescription(BuildContext context) { diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart index f90df7b..6ccf750 100644 --- a/lib/presentation/lab/lab_results/lab_result_list_item.dart +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart' ; -import 'package:flutter/src/widgets/framework.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/lab/search_lab_report.dart b/lib/presentation/lab/search_lab_report.dart index f1fd389..bf27b91 100644 --- a/lib/presentation/lab/search_lab_report.dart +++ b/lib/presentation/lab/search_lab_report.dart @@ -8,7 +8,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:sizer/sizer.dart'; class SearchLabResultsContent extends StatefulWidget { final List labSuggestionsList; diff --git a/lib/presentation/medical_file/eye_measurement_details_page.dart b/lib/presentation/medical_file/eye_measurement_details_page.dart new file mode 100644 index 0000000..0662cb1 --- /dev/null +++ b/lib/presentation/medical_file/eye_measurement_details_page.dart @@ -0,0 +1,197 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.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/custom_tab_bar.dart'; +import 'package:provider/provider.dart'; + +class EyeMeasurementDetailsPage extends StatelessWidget { + EyeMeasurementDetailsPage({super.key, required this.patientAppointmentHistoryResponseModel}); + + final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; + late BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.eyeMeasurements.tr(), + child: SingleChildScrollView( + child: Consumer(builder: (context, myAppointmentsVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + myAppointmentsViewModel: myAppointmentsVM, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: false, + isFromMedicalReport: true, + isForEyeMeasurements: true, + ), + ), + SizedBox(height: 16.h), + CustomTabBar( + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, LocaleKeys.classes.tr()), + CustomTabBarModel(null, LocaleKeys.contactLens.tr()), + ], + onTabChange: (index) { + myAppointmentsVM.onEyeMeasurementsTabChanged(index); + }, + ), + SizedBox(height: 12.h), + getSelectedTabContent(myAppointmentsVM), + ], + ).paddingSymmetrical(24.w, 0); + }), + ), + ), + ); + } + + Widget getSelectedTabContent(MyAppointmentsViewModel myAppointmentsVM) { + switch (myAppointmentsVM.eyeMeasurementsTabSelectedIndex) { + case 0: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.rightEye.tr().toText14(isBold: true), + SizedBox(height: 16.h), + getRow(LocaleKeys.sphere.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyeSpherical}', '-'), + getRow(LocaleKeys.cylinder.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyeCylinder}', '-'), + getRow(LocaleKeys.axis.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyeAxis}', '-'), + getRow(LocaleKeys.prism.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyePrism}', '-'), + getRow(LocaleKeys.va.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyeVA}', '-'), + getRow(LocaleKeys.remarks.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].rightEyeRemarks}', '-', isLast: true), + ], + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.leftEye.tr().needTranslation.toText14(isBold: true), + SizedBox(height: 16.h), + getRow(LocaleKeys.sphere.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyeSpherical}', '-'), + getRow(LocaleKeys.cylinder.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyeCylinder}', '-'), + getRow(LocaleKeys.axis.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyeAxis}', '-'), + getRow(LocaleKeys.prism.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyePrism}', '-'), + getRow(LocaleKeys.va.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyeVA}', '-'), + getRow(LocaleKeys.remarks.tr(), '${patientAppointmentHistoryResponseModel.listHISGetGlassPrescription![0].leftEyeRemarks}', '-', isLast: true), + ], + ), + ), + SizedBox(height: 24.h), + ], + ); + case 1: + return Column( + children: [ + Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.rightEye.tr().toText14(isBold: true), + SizedBox(height: 16.h), + getRow(LocaleKeys.brand.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].brand}', ''), + getRow('B.C', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].baseCurve}', ''), + getRow(LocaleKeys.power.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].power}', ''), + getRow(LocaleKeys.diameter.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].diameter}', ''), + getRow('OZ', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].oZ}', ''), + getRow('CT', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].cT}', ''), + getRow('Blend', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].blend}', ''), + getRow(LocaleKeys.remarks.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![0].remarks}', '', isLast: true), + ], + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.leftEye.tr().needTranslation.toText14(isBold: true), + SizedBox(height: 16.h), + getRow(LocaleKeys.brand.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].brand}', ''), + getRow('B.C', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].baseCurve}', ''), + getRow(LocaleKeys.power.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].power}', ''), + getRow(LocaleKeys.diameter.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].diameter}', ''), + getRow('OZ', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].oZ}', ''), + getRow('CT', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].cT}', ''), + getRow('Blend', '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].blend}', ''), + getRow(LocaleKeys.remarks.tr(), '${patientAppointmentHistoryResponseModel.listHISGetContactLensPrescription![1].remarks}', '', isLast: true), + ], + ), + ), + SizedBox(height: 24.h), + ], + ); + default: + return Container(); + } + } + + Widget getRow(String title, String val1, String val2, {bool isLast = false}) => Padding( + padding: EdgeInsets.only(left: 8.w, right: 8.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 2, child: title.toText11(weight: FontWeight.w500)), + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SizedBox(width: 120.w, child: (val1 == 'null' ? '-' : val1).toText10(isBold: true, textOverflow: TextOverflow.clip)), + (val2 == 'null' ? '-' : val2).toText10(isBold: true, textOverflow: TextOverflow.ellipsis), + ], + ), + ) + ], + ), + ), + isLast + ? Container( + height: 4, + ) + : Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 2.h) + ], + ), + ); +} diff --git a/lib/presentation/medical_file/eye_measurements_appointments_page.dart b/lib/presentation/medical_file/eye_measurements_appointments_page.dart new file mode 100644 index 0000000..3b82ad4 --- /dev/null +++ b/lib/presentation/medical_file/eye_measurements_appointments_page.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; + +class EyeMeasurementsAppointmentsPage extends StatelessWidget { + EyeMeasurementsAppointmentsPage({super.key}); + + late BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Eye Measurements", + child: SingleChildScrollView( + child: Consumer(builder: (context, myAppointmentsVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ListView.separated( + scrollDirection: Axis.vertical, + itemCount: myAppointmentsVM.isEyeMeasurementsAppointmentsLoading + ? 5 + : myAppointmentsVM.patientEyeMeasurementsAppointmentsHistoryList.isNotEmpty + ? myAppointmentsVM.patientEyeMeasurementsAppointmentsHistoryList.length + : 1, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(left: 24.h, right: 24.h), + itemBuilder: (context, index) { + return myAppointmentsVM.isEyeMeasurementsAppointmentsLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), + myAppointmentsViewModel: myAppointmentsVM, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: true, + isFromHomePage: false, + ), + ) + : myAppointmentsVM.patientEyeMeasurementsAppointmentsHistoryList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientEyeMeasurementsAppointmentsHistoryList[index], + myAppointmentsViewModel: myAppointmentsVM, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: false, + isForEyeMeasurements: true, + ), + ), + ), + ), + ) + : Utils.getNoDataWidget(context, noDataText: "No Ophthalmology appointments found...".needTranslation); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 60.h), + ], + ); + }), + ), + ), + ); + } +} diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index adc2ba7..6dde958 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_config.dart'; @@ -15,35 +17,49 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/allergies/allergies_list_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_approvals_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/medical_file/eye_measurements_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/vaccine_list_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/medical_reports_page.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_report/monthly_report.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -51,11 +67,11 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; +import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; import 'package:provider/provider.dart'; import '../prescriptions/prescription_detail_page.dart'; @@ -75,24 +91,53 @@ class _MedicalFilePageState extends State { late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; + late MyInvoicesViewModel myInvoicesViewModel; + late HmgServicesViewModel hmgServicesViewModel; + late PrescriptionsViewModel prescriptionsViewModel; + late MonthlyReportViewModel monthlyReportViewModel; + + final CacheService cacheService = GetIt.instance(); int currentIndex = 0; + // Used to make the PageView height follow the card's intrinsic height + final GlobalKey _vitalSignMeasureKey = GlobalKey(); + double? _vitalSignMeasuredHeight; + @override void initState() { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - labViewModel.initLabProvider(); + myAppointmentsViewModel.getPatientMyDoctors(); + prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); medicalFileViewModel.onTabChanged(0); + // Load vital signs + hmgServicesViewModel.getPatientVitalSign(); } }); super.initState(); } + void _scheduleVitalSignMeasure() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _vitalSignMeasureKey.currentContext; + if (ctx == null) return; + final box = ctx.findRenderObject(); + if (box is RenderBox) { + final h = box.size.height; + if (h > 0 && h != _vitalSignMeasuredHeight) { + setState(() { + _vitalSignMeasuredHeight = h; + }); + } + } + }); + } + @override Widget build(BuildContext context) { labViewModel = Provider.of(context, listen: false); @@ -100,29 +145,38 @@ class _MedicalFilePageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); + myInvoicesViewModel = Provider.of(context, listen: false); + hmgServicesViewModel = Provider.of(context, listen: false); + prescriptionsViewModel = Provider.of(context, listen: false); + monthlyReportViewModel = Provider.of(context, listen: false); NavigationService navigationService = getIt.get(); return CollapsingListView( - title: "Medical File".needTranslation, + // title: "Medical File".needTranslation, + title: LocaleKeys.medicalFile.tr(context: context), trailing: Row( children: [ Wrap( + spacing: -8.h, + // runSpacing: 0.h, children: [ Utils.buildImgWithAssets( icon: AppAssets.babyGirlImg, - height: 32.h, - border: 1.5, + height: 28.h, + width: 28.w, + border: 1, fit: BoxFit.contain, borderRadius: 50.r, ), Utils.buildImgWithAssets( icon: AppAssets.femaleImg, - height: 32.h, - border: 1.5, + height: 28.h, + width: 28.w, + border: 1, borderRadius: 50.r, fit: BoxFit.contain, ), Utils.buildImgWithAssets( - icon: AppAssets.male_img, + icon: AppAssets.maleImg, height: 32.h, border: 1.5, borderRadius: 50.r, @@ -136,142 +190,326 @@ class _MedicalFilePageState extends State { ).withHorizontalPadding(24.w).onPress(() { DialogService dialogService = getIt.get(); dialogService.showFamilyBottomSheetWithoutH( - label: "Who do you want to book for?".needTranslation, + label: "Family Files".needTranslation, message: "This clinic or doctor is only available for the below eligible profiles.".needTranslation, onSwitchPress: (FamilyFileResponseModelLists profile) { medicalFileViewModel.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); }, profiles: medicalFileViewModel.patientFamilyFiles); }), - isLeading: false, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: "Type any record".needTranslation, - controller: TextEditingController(), - keyboardType: TextInputType.number, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - isAllowLeadingIcon: true, - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), - leadingIcon: AppAssets.student_card, - ).paddingSymmetrical(24.w, 0.0), - SizedBox(height: 16.h), - Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r), - child: Padding( - padding: EdgeInsets.all(16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + isLeading: true, + leadingCallback: () { + Navigator.pushAndRemoveUntil( + navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" + .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 2), + ), + SizedBox(height: 4.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.w, + runSpacing: 6.w, + children: [ + AppCustomChipWidget( + icon: AppAssets.file_icon, + labelText: "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + onChipTap: () { + navigationService.pushPage( + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, + ), + ); + }, + ), + AppCustomChipWidget( + icon: AppAssets.checkmark_icon, + labelText: LocaleKeys.verified.tr(context: context), + iconColor: AppColors.successColor, + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + ), + ], + ), + ], + ) + ], + ), + SizedBox(height: 16.h), + Divider(color: AppColors.dividerColor, height: 1.h), + SizedBox(height: 16.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "${appState.getAuthenticatedUser()!.age} Years Old", + labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), + ), + AppCustomChipWidget( + icon: AppAssets.blood_icon, + labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup}", + iconColor: AppColors.primaryRedColor, + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + padding: EdgeInsets.zero, + ), + Consumer(builder: (context, insuranceVM, child) { + return AppCustomChipWidget( + icon: insuranceVM.isInsuranceExpired ? AppAssets.cancel_circle_icon : AppAssets.insurance_active_icon, + labelText: insuranceVM.isInsuranceExpired ? "Insurance Expired".needTranslation : "Insurance Active".needTranslation, + iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, + textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, + iconSize: 12.w, + backgroundColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + ); + }), + ], + ), + ], + ), + ), + ).paddingSymmetrical(24.w, 0.0), + SizedBox(height: 16.h), + + // Vital Signs Section + Consumer(builder: (context, hmgServicesVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, - width: 56.w, height: 56.h), - SizedBox(width: 8.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" - .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), - SizedBox(height: 4.h), - Wrap( - direction: Axis.horizontal, - spacing: 4.w, - runSpacing: 6.w, - children: [ - AppCustomChipWidget( - icon: AppAssets.file_icon, - labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", - labelPadding: EdgeInsetsDirectional.only(end: 6.w), - onChipTap: () { - navigationService.pushPage( - page: FamilyMedicalScreen( - profiles: medicalFileViewModel.patientFamilyFiles, - onSelect: (FamilyFileResponseModelLists p1) {}, - )); - }, - ), - AppCustomChipWidget( - icon: AppAssets.checkmark_icon, - labelText: LocaleKeys.verified.tr(context: context), - iconColor: AppColors.successColor, - labelPadding: EdgeInsetsDirectional.only(end: 6.w), - ), - ], - ), - ], - ) + LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), ], ), - SizedBox(height: 16.h), - Divider(color: AppColors.dividerColor, height: 1.h), - SizedBox(height: 16.h), - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "${appState.getAuthenticatedUser()!.age} Years Old", - labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), + ], + ).paddingSymmetrical(0.w, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }), + SizedBox(height: 16.h), + // Make this section dynamic-height (no fixed 160.h) + LayoutBuilder( + builder: (context, constraints) { + if (hmgServicesVM.isVitalSignLoading) { + return _buildVitalSignShimmer(); + } + if (hmgServicesVM.vitalSignList.isEmpty) { + return Container( + padding: EdgeInsets.all(16.w), + width: MediaQuery.of(context).size.width, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, ), - AppCustomChipWidget( - icon: AppAssets.blood_icon, - labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup.isEmpty}", - iconColor: AppColors.primaryRedColor, - labelPadding: EdgeInsetsDirectional.only(end: 8.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "No vital signs recorded yet".needTranslation.toText12(isCenter: true), + ], ), - Consumer(builder: (context, insuranceVM, child) { - return AppCustomChipWidget( - icon: insuranceVM.isInsuranceExpired ? AppAssets.cancel_circle_icon : AppAssets.insurance_active_icon, - labelText: insuranceVM.isInsuranceExpired ? "Insurance Expired".needTranslation : "Insurance Active".needTranslation, - iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, - textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, - iconSize: 12.w, - backgroundColor: - insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), - labelPadding: EdgeInsetsDirectional.only(end: 8.w), - ); - }), - ], - ), - ], + ); + } + + // The cards define their own height; measure the first rendered page once + _scheduleVitalSignMeasure(); + final double hostHeight = _vitalSignMeasuredHeight ?? (160.h); + + return SizedBox( + height: hostHeight, + child: PageView( + controller: hmgServicesVM.vitalSignPageController, + onPageChanged: (index) { + hmgServicesVM.setVitalSignCurrentPage(index); + _scheduleVitalSignMeasure(); + }, + children: _buildVitalSignPages( + vitalSign: hmgServicesVM.vitalSignList.first, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }, + measureKey: _vitalSignMeasureKey, + currentPageIndex: hmgServicesVM.vitalSignCurrentPage, + ), + ), + ); + }, ), + if (!hmgServicesVM.isVitalSignLoading && hmgServicesVM.vitalSignList.isNotEmpty) ...[ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 2, // 2 pages (BMI+Height on page 1, Weight+BP on page 2) + (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 3.w), + width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w, + height: 8.h, + decoration: BoxDecoration( + color: hmgServicesVM.vitalSignCurrentPage == index + ? AppColors.primaryRedColor + : AppColors.dividerColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ), + ], + ], + ).paddingSymmetrical(24.w, 0.0); + }), + SizedBox(height: 16.h), + + TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: "Type any record".needTranslation, + controller: TextEditingController(), + keyboardType: TextInputType.number, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + leadingIcon: AppAssets.search_icon, + hintColor: AppColors.textColor, + ).paddingSymmetrical(24.w, 0.0), + SizedBox(height: 16.h), + // Using CustomExpandableList + CustomExpandableList( + expansionMode: ExpansionMode.exactlyOne, + dividerColor: Color(0xff2b353e1a), + itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), + items: [ + ExpandableListItem( + title: "Medical Services".toText18(weight: FontWeight.w600), + children: [ + SizedBox(height: 10.h), + getSelectedTabData(0), + ], ), - ).paddingSymmetrical(24.w, 0.0), - SizedBox(height: 16.h), - Consumer(builder: (context, medicalFileVM, child) { - return Column( + ExpandableListItem( + title: "Medical Reports".toText18( + weight: FontWeight.w600, + ), + expandedBackgroundColor: Colors.transparent, + children: [ + SizedBox(height: 10.h), + getSelectedTabData(2), + ]), + ExpandableListItem( + title: "Insurance & Payments".toText18(weight: FontWeight.w600), + expandedBackgroundColor: Colors.transparent, children: [ - CustomTabBar( - activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), - tabs: [ - CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), - CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), - CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation), - CustomTabBarModel(AppAssets.more, "More".needTranslation), - ], - onTabChange: (index) { - medicalFileVM.onTabChanged(index); - }, - ).paddingSymmetrical(24.w, 0.0), - SizedBox(height: 24.h), - getSelectedTabData(medicalFileVM.selectedTabIndex), + SizedBox(height: 10.h), + getSelectedTabData(1), ], - ); - }), - ], - ), + ), + ExpandableListItem( + title: "Tracker & Others".toText18(weight: FontWeight.w600), + expandedBackgroundColor: Colors.transparent, + children: [ + SizedBox(height: 10.h), + getSelectedTabData(3), + ], + ), + ], + theme: ExpandableListTheme.custom( + defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor), + ), + ).paddingSymmetrical(16.w, 0.0), + + // ExpansionTileList( + // shrinkWrap: true, + // trailing: Icon(Icons.arrow_drop_down), + // expansionMode: ExpansionMode.exactlyOne, + // children: [ + // ExpansionTile( + // title: "Medical Services".toText20(weight: FontWeight.w600, color: AppColors.textColor), + // iconColor: Color(0xFF2E3039), + // trailing: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w), + // children: [Text('Child 1')], + // ), + // ExpansionTile( + // title: "Medical Reports".toText20(weight: FontWeight.w600, color: AppColors.textColor), + // children: [Text('Child 2')], + // ), + // ExpansionTile( + // title: "Insurance & Payments".toText20(weight: FontWeight.w600, color: AppColors.textColor), + // children: [Text('Child 3')], + // ), + // ExpansionTile( + // title: "Tracker & Others".toText20(weight: FontWeight.w600, color: AppColors.textColor), + // children: [Text('Child 4')], + // ), + // ], + // ), + // Consumer(builder: (context, medicalFileVM, child) { + // return Column( + // children: [ + // CustomTabBar( + // activeTextColor: AppColors.primaryRedColor, + // activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + // tabs: [ + // CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), + // CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), + // CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation), + // CustomTabBarModel(AppAssets.more, "More".needTranslation), + // ], + // onTabChange: (index) { + // medicalFileVM.onTabChanged(index); + // }, + // ).paddingSymmetrical(24.w, 0.0), + // SizedBox(height: 24.h), + // getSelectedTabData(medicalFileVM.selectedTabIndex), + // ], + // ); + // }), + ], ), ); } @@ -326,7 +564,7 @@ class _MedicalFilePageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Appointments & visits".needTranslation.toText18(isBold: true), + "Appointments & visits".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), Row( children: [ LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), @@ -335,7 +573,7 @@ class _MedicalFilePageState extends State { ], ), ], - ).paddingSymmetrical(24.w, 0.h).onPress(() { + ).paddingSymmetrical(0.w, 0.h).onPress(() { Navigator.of(context).push( CustomPageRoute( page: MyAppointmentsPage(), @@ -346,7 +584,7 @@ class _MedicalFilePageState extends State { Consumer(builder: (context, myAppointmentsVM, child) { // Provide an explicit height so the horizontal ListView has a bounded height return SizedBox( - height: 190.h, + height: 192.h, child: myAppointmentsVM.isMyAppointmentsLoading ? MedicalFileAppointmentCard( patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), @@ -358,8 +596,7 @@ class _MedicalFilePageState extends State { ? Container( padding: EdgeInsets.all(12.w), width: MediaQuery.of(context).size.width, - decoration: - RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), @@ -369,6 +606,7 @@ class _MedicalFilePageState extends State { CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () { + getIt.get().onTabChanged(0); Navigator.of(context).push( CustomPageRoute( page: BookAppointmentPage(), @@ -401,26 +639,58 @@ class _MedicalFilePageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: MedicalFileAppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - onRescheduleTap: () { - openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); - }, - onAskDoctorTap: () {}, - )), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: MedicalFileAppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + myAppointmentsViewModel: myAppointmentsViewModel, + onRescheduleTap: () { + openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); + }, + onAskDoctorTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); + await myAppointmentsViewModel.isDoctorAvailable( + projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, + doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, + clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, + onSuccess: (value) async { + if (value) { + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + LoaderBottomSheet.hideLoader(); + print("Doctor is not available"); + } + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + }, + ), + ), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), ), - ).paddingSymmetrical(24.w, 0); + ).paddingSymmetrical(0.w, 0.h); }), - SizedBox(height: 24.h), - "Lab & Radiology".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 10.h), + "Lab & Radiology".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), SizedBox(height: 16.h), Row( children: [ @@ -439,11 +709,12 @@ class _MedicalFilePageState extends State { ); }), ), - SizedBox(width: 16.h), + SizedBox(width: 8.w), Expanded( child: LabRadCard( icon: AppAssets.radiology_icon, - labelText: LocaleKeys.radiology.tr(context: context), + labelText: "${LocaleKeys.radiology.tr(context: context)} Results".needTranslation, + // labOrderTests: ["Complete blood count", "Creatinine", "Blood Sugar", // labOrderTests: ["Chest X-ray", "Abdominal Ultrasound", "Dental X-ray"], labOrderTests: [], isLoading: false, @@ -456,25 +727,22 @@ class _MedicalFilePageState extends State { }), ), ], - ).paddingSymmetrical(24.w, 0.h), + ).paddingSymmetrical(0.w, 0.h), SizedBox(height: 24.h), - "Active Medications & Prescriptions".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + "Active Medications & Prescriptions".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), SizedBox(height: 16.h), Consumer(builder: (context, prescriptionVM, child) { return prescriptionVM.isPrescriptionsOrdersLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.w, 0.h) + ? const CommonShimmerWidget().paddingSymmetrical(0.w, 0.h) : prescriptionVM.patientPrescriptionOrders.isNotEmpty ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, - borderRadius: 12.r, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r, hasShadow: false), child: Padding( padding: EdgeInsets.all(16.w), child: Column( children: [ ListView.separated( - itemCount: prescriptionVM.patientPrescriptionOrders.length, + itemCount: prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, shrinkWrap: true, padding: EdgeInsets.only(left: 0, right: 8.w), physics: NeverScrollableScrollPhysics(), @@ -505,13 +773,11 @@ class _MedicalFilePageState extends State { spacing: 3.w, runSpacing: 4.w, children: [ - AppCustomChipWidget( - labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), + AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), AppCustomChipWidget( icon: AppAssets.doctor_calendar_icon, labelText: DateUtil.formatDateToDate( - DateUtil.convertStringToDate( - prescriptionVM.patientPrescriptionOrders[index].appointmentDate), + DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate), false, ), ), @@ -524,77 +790,71 @@ class _MedicalFilePageState extends State { Transform.flip( flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - width: 15.w, - height: 15.h, - fit: BoxFit.contain, - iconColor: AppColors.textColor)), + icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), ], ).onPress(() { prescriptionVM.setPrescriptionsDetailsLoading(); Navigator.of(context).push( CustomPageRoute( - page: PrescriptionDetailPage( - prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), + page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), ), ); }), ), + )); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 16.h), + const Divider(color: AppColors.dividerColor), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "All Prescriptions".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionsListPage(), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 40.h, + icon: AppAssets.requests, + iconColor: AppColors.primaryRedColor, + iconSize: 16.w, + ), ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - SizedBox(height: 24.h), - const Divider(color: AppColors.dividerColor), - SizedBox(height: 24.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "All Prescriptions".needTranslation, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 56.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.w, - ), - ), - SizedBox(width: 10.w), - Expanded( - child: CustomButton( - text: "All Medications".needTranslation, - onPressed: () {}, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.h, - height: 56.h, - icon: AppAssets.all_medications_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, + SizedBox(width: 6.w), + Expanded( + child: CustomButton( + text: "All Medications".needTranslation, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.h, + height: 40.h, + icon: AppAssets.all_medications_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ), ), - ), - ], - ), - ], + ], + ), + ], ), ), - ).paddingSymmetrical(24.w, 0.h) + ).paddingSymmetrical(0.w, 0.h) : Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -608,14 +868,14 @@ class _MedicalFilePageState extends State { width: 62.w, height: 62.h, ), - ).paddingSymmetrical(24.w, 0.h); + ).paddingSymmetrical(0.w, 0.h); }), SizedBox(height: 24.h), //My Doctor Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.myDoctor.tr(context: context).toText18(isBold: true), + LocaleKeys.myDoctor.tr(context: context).toText16(weight: FontWeight.w500, letterSpacing: -0.2), Row( children: [ LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), @@ -631,7 +891,7 @@ class _MedicalFilePageState extends State { ); }), ], - ).paddingSymmetrical(24.w, 0.h), + ).paddingSymmetrical(0.w, 0.h), SizedBox(height: 16.h), Consumer(builder: (context, myAppointmentsVM, child) { return myAppointmentsVM.isPatientMyDoctorsLoading @@ -645,10 +905,7 @@ class _MedicalFilePageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith") - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: true), + ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), ], ) : myAppointmentsVM.patientMyDoctorsList.isEmpty @@ -666,9 +923,9 @@ class _MedicalFilePageState extends State { width: 62.w, height: 62.h, ), - ).paddingSymmetrical(24.w, 0.h) + ).paddingSymmetrical(0.w, 0.h) : SizedBox( - height: 200.h, + height: 100.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: myAppointmentsVM.patientMyDoctorsList.length, @@ -726,22 +983,21 @@ class _MedicalFilePageState extends State { }); }), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), - ).paddingSymmetrical(24.w, 0); + ).paddingSymmetrical(0.w, 0); }), SizedBox(height: 24.h), - "Others".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + "Others".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), SizedBox(height: 16.h), GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, - mainAxisExtent: 130.h, + mainAxisExtent: 115.h, ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, @@ -754,7 +1010,16 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.eye_result_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + myAppointmentsViewModel.setIsEyeMeasurementsAppointmentsLoading(true); + myAppointmentsViewModel.onEyeMeasurementsTabChanged(0); + myAppointmentsViewModel.getPatientEyeMeasurementAppointments(); + Navigator.of(context).push( + CustomPageRoute( + page: EyeMeasurementsAppointmentsPage(), + ), + ); + }), MedicalFileCard( label: "Allergy Info".needTranslation, textColor: AppColors.blackColor, @@ -762,7 +1027,14 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.allergy_info_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + medicalFileViewModel.getPatientAllergiesList(); + Navigator.of(context).push( + CustomPageRoute( + page: AllergiesListPage(), + ), + ); + }), MedicalFileCard( label: "Vaccine Info".needTranslation, textColor: AppColors.blackColor, @@ -778,7 +1050,7 @@ class _MedicalFilePageState extends State { ); }), ], - ).paddingSymmetrical(24.w, 0.0), + ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 24.h), ], ); @@ -793,7 +1065,7 @@ class _MedicalFilePageState extends State { labOrder: null, index: index, isLoading: true, - ).paddingSymmetrical(24.w, 0.0) + ).paddingSymmetrical(0.w, 0.0) : insuranceVM.patientInsuranceList.isNotEmpty ? PatientInsuranceCard( insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, @@ -820,14 +1092,9 @@ class _MedicalFilePageState extends State { text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", onPressed: () { insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); - insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), - appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, - child: PatientInsuranceCardUpdateCard(), - callBackFunc: () {}, - title: "", - isCloseButtonVisible: false, - isFullScreen: false); + insuranceViewModel.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); }, backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), borderColor: AppColors.bgGreenColor.withOpacity(0.0), @@ -839,15 +1106,15 @@ class _MedicalFilePageState extends State { height: isFoldable ? 50.h : 40.h, ).paddingOnly(left: 12.w, right: 12.w, bottom: 12.h), ), - ).paddingSymmetrical(24.w, 0.h); + ).paddingSymmetrical(0.w, 0.h); }), SizedBox(height: 10.h), GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, - mainAxisExtent: 150.h, + mainAxisExtent: 120.h, ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.only(top: 12.h), @@ -857,7 +1124,7 @@ class _MedicalFilePageState extends State { label: "Update Insurance".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.update_insurance_icon, isLargeText: true, iconSize: 36.w, ).onPress(() { @@ -867,7 +1134,7 @@ class _MedicalFilePageState extends State { label: "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.approvals.tr(context: context)}", textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.insurance_approval_icon, isLargeText: true, iconSize: 36.w, ).onPress(() { @@ -881,20 +1148,32 @@ class _MedicalFilePageState extends State { label: "My Invoices List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.invoices_list_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MyInvoicesList(), + ), + ); + }), MedicalFileCard( label: "Ancillary Orders List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.ancillary_orders_list_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: ToDoPage(), + ), + ); + }), ], - ).paddingSymmetrical(24.w, 0.0), + ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 16.h), ], ); @@ -907,12 +1186,12 @@ class _MedicalFilePageState extends State { ? PatientSickLeaveCard( patientSickLeavesResponseModel: PatientSickLeavesResponseModel(), isLoading: true, - ).paddingSymmetrical(24.w, 0.0) + ).paddingSymmetrical(0.w, 0.0) : medicalFileVM.patientSickLeaveList.isNotEmpty ? PatientSickLeaveCard( patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, isLoading: false, - ).paddingSymmetrical(24.w, 0.0) + ).paddingSymmetrical(0.w, 0.0) : Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -926,15 +1205,15 @@ class _MedicalFilePageState extends State { width: 62.w, height: 62.h, ), - ).paddingSymmetrical(24.w, 0.h); + ).paddingSymmetrical(0.w, 0.h); }), SizedBox(height: 16.h), GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, - mainAxisExtent: 140.h, + mainAxisExtent: 110.h, ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, @@ -944,17 +1223,24 @@ class _MedicalFilePageState extends State { label: LocaleKeys.monthlyReports.tr(context: context), textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + monthlyReportViewModel.setHealthSummaryEnabled(cacheService.getBool(key: CacheConst.isMonthlyReportEnabled) ?? false); + Navigator.of(context).push( + CustomPageRoute( + page: MonthlyReport(), + ), + ); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.allergy_info_icon, + svgIcon: AppAssets.medical_reports_icon, isLargeText: true, - iconSize: 36.h, + iconSize: 36.w, ).onPress(() { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); @@ -968,7 +1254,7 @@ class _MedicalFilePageState extends State { label: "Sick Leave Report".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.vaccine_info_icon, + svgIcon: AppAssets.sick_leave_report_icon, isLargeText: true, iconSize: 36.h, ).onPress(() { @@ -979,14 +1265,360 @@ class _MedicalFilePageState extends State { ); }), ], - ).paddingSymmetrical(24.w, 0.0), + ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 24.h), ], ); case 3: - return Container(); + // Trackers Tab Data + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Health Trackers".needTranslation.toText16(weight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(height: 16.h), + GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10.h, + mainAxisSpacing: 16.w, + mainAxisExtent: 115.h, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: "Blood Sugar".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.blood_sugar_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + MedicalFileCard( + label: "Blood Pressure".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.lab_result_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + MedicalFileCard( + label: "Weight Tracker".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.weight_tracker_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + ], + ).paddingSymmetrical(0.w, 0.0), + SizedBox(height: 16.h), + Row( + children: [ + "Others".needTranslation.toText16(weight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(height: 16.h), + GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10.h, + mainAxisSpacing: 16.w, + mainAxisExtent: 115.h, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: "Ask Your Doctor".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.ask_doctor_medical_file_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + MedicalFileCard( + label: "Internet Pairing".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.internet_pairing_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + ], + ).paddingSymmetrical(0.w, 0.0), + SizedBox(height: 24.h), + ], + ); default: return Container(); } } + + // Build shimmer for vital signs + Widget _buildVitalSignShimmer() { + return Row( + children: [ + Expanded(child: _buildSingleShimmerCard()), + SizedBox(width: 12.w), + Expanded(child: _buildSingleShimmerCard()), + ], + ); + } + + Widget _buildSingleShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Icon shimmer at top + Container( + width: 44.w, + height: 44.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 16.h), + // Label shimmer + Container( + width: 70.w, + height: 12.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Value shimmer (larger) + Container( + width: 60.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 12.h), + // Bottom row with chip and arrow + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60.w, + height: 20.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + Container( + width: 16.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ], + ), + ], + ), + ), + ); + } + + // Build pages with 2 cards each + List _buildVitalSignPages({ + required VitalSignResModel vitalSign, + required VoidCallback onTap, + required GlobalKey measureKey, + required int currentPageIndex, + }) { + return [ + // Page 1: BMI + Height + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: "BMI", + value: vitalSign.bodyMassIndex?.toString() ?? '--', + unit: '', + status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.heightVital, + label: "Height", + value: vitalSign.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: onTap, + ), + ), + ], + ), + // Page 2: Weight + Blood Pressure + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: "Weight", + value: vitalSign.weightKg?.toString() ?? '--', + unit: 'kg', + status: vitalSign.weightKg != null ? "Normal" : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: "Blood Pressure", + value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ), + onTap: onTap, + ), + ), + ], + ), + ]; + } + + String _getBMIStatus(dynamic bmi) { + return VitalSignUiModel.bmiStatus(bmi); + } + + String? _getBloodPressureStatus({dynamic systolic, dynamic diastolic}) { + return VitalSignUiModel.bloodPressureStatus(systolic: systolic, diastolic: diastolic); + } + + Widget _buildVitalSignCard({ + required String icon, + required String label, + required String value, + required String unit, + required String? status, + required VoidCallback onTap, + }) { + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + // Same styling used originally for vitals in MedicalFilePage + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 14.h), + + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], + ), + ), + SizedBox(height: 8.h), + + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 18.w, + height: 18.h, + iconColor: AppColors.textColorLight, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ); + } } + + + diff --git a/lib/presentation/medical_file/patient_sickleaves_list_page.dart b/lib/presentation/medical_file/patient_sickleaves_list_page.dart index a28f518..ef5aaeb 100644 --- a/lib/presentation/medical_file/patient_sickleaves_list_page.dart +++ b/lib/presentation/medical_file/patient_sickleaves_list_page.dart @@ -3,6 +3,10 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -12,6 +16,9 @@ import 'package:hmg_patient_app_new/features/medical_file/models/patient_sicklea import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; import 'widgets/patient_sick_leave_card.dart'; @@ -24,6 +31,7 @@ class PatientSickleavesListPage extends StatefulWidget { } class _PatientSickleavesListPageState extends State { + int? expandedIndex; late MedicalFileViewModel medicalFileViewModel; @override @@ -37,51 +45,257 @@ class _PatientSickleavesListPageState extends State { @override Widget build(BuildContext context) { + AppState appState = getIt.get(); medicalFileViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}", child: SingleChildScrollView( - child: Consumer(builder: (context, medicalFileVM, child) { + child: Consumer(builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListView.separated( - scrollDirection: Axis.vertical, - itemCount: medicalFileVM.isPatientSickLeaveListLoading - ? 3 - : medicalFileVM.patientSickLeaveList.isNotEmpty - ? medicalFileVM.patientSickLeaveList.length + SizedBox(height: 16.h), + // Clinic & Hospital Sort + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + model.setIsSickLeavesSortByClinic(true); + }, + backgroundColor: model.isSickLeavesSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + model.setIsSickLeavesSortByClinic(false); + }, + backgroundColor: model.isSickLeavesSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: model.isSickLeavesSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: model.isSickLeavesSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 20.h), + // Expandable list + ListView.builder( + itemCount: model.isPatientSickLeaveListLoading + ? 4 + : model.patientSickLeaveList.isNotEmpty + ? model.patientSickLeavesViewList.length : 1, - shrinkWrap: true, physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), itemBuilder: (context, index) { - return medicalFileVM.isPatientSickLeaveListLoading + final isExpanded = expandedIndex == index; + return model.isPatientSickLeaveListLoading ? PatientSickLeaveCard( patientSickLeavesResponseModel: PatientSickLeavesResponseModel(), isLoading: true, - ).paddingSymmetrical(24.h, 0.0) - : medicalFileVM.patientSickLeaveList.isNotEmpty + ).paddingSymmetrical(0.h, 12.h) + : model.patientSickLeaveList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( - child: PatientSickLeaveCard( - patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, - isLoading: false, - isSickLeaveListPage: true, - ).paddingSymmetrical(24.h, 0.0), + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${model.patientSickLeavesViewList[index].sickLeavesList!.length} ${LocaleKeys.sickSubtitle.tr(context: context)} Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + model.patientSickLeavesViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...model.patientSickLeavesViewList[index].sickLeavesList!.map((sickLeave) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + sickLeave.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: sickLeave.doctorName!.toText14(weight: FontWeight.w500)), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(sickLeave.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: model.isSickLeavesSortByClinic ? sickLeave.projectName! : sickLeave.clinicName!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: "${sickLeave.sickLeaveDays} Days", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Expanded( + flex: 6, + child: CustomButton( + text: "Download Report".needTranslation, + onPressed: () async { + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.getPatientSickLeavePDF(sickLeave, appState.getAuthenticatedUser()!).then((val) async { + LoaderBottomSheet.hideLoader(); + if (medicalFileViewModel.patientSickLeavePDFBase64.isNotEmpty) { + String path = await Utils.createFileFromString(medicalFileViewModel.patientSickLeavePDFBase64, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + debugPrint("Error opening file: $ex"); + } + } + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.download, + iconColor: AppColors.primaryRedColor, + iconSize: 14.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any sick leaves yet.".needTranslation); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), - ), - SizedBox(height: 60.h), + ).paddingSymmetrical(24.h, 0.h), ], ); }), diff --git a/lib/presentation/medical_file/widgets/lab_rad_card.dart b/lib/presentation/medical_file/widgets/lab_rad_card.dart index 42f5bff..f31c90b 100644 --- a/lib/presentation/medical_file/widgets/lab_rad_card.dart +++ b/lib/presentation/medical_file/widgets/lab_rad_card.dart @@ -27,57 +27,27 @@ class LabRadCard extends StatelessWidget { Widget build(BuildContext context) { AppState appState = getIt.get(); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 18.r, hasShadow: false), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - Row( - children: [ - Utils.buildSvgWithAssets( - icon: icon, - width: 40.w, - height: 40.h, - fit: BoxFit.contain, - ).toShimmer2(isShow: false, radius: 12.r), - SizedBox(width: 8.w), - Flexible(child: labelText.toText14(isBold: true).toShimmer2(isShow: false, radius: 6.r, height: 32.h)), - ], + Utils.buildSvgWithAssets( + icon: icon, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).toShimmer2(isShow: false, radius: 12.r), + SizedBox(width: 8.w), + Flexible( + child: labelText.toText12(isBold: true, maxLine: 2), + ), + SizedBox(width: 12.w), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 10.w, height: 10.h, fit: BoxFit.contain, iconColor: AppColors.textColor), ), - SizedBox(height: 16.h), - labOrderTests.isNotEmpty - ? ListView.separated( - scrollDirection: Axis.vertical, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (cxt, index) { - return labOrderTests[index] - .toText12(isBold: true, maxLine: 1) - .toShimmer2(isShow: false, radius: 6.r, height: 24.h, width: 120.w) - .toShimmer2(isShow: isLoading); - }, - separatorBuilder: (cxt, index) => SizedBox(height: 8.h), - itemCount: 3, - ) - : "You don't have any records yet".needTranslation.toText13( - color: AppColors.greyTextColor, - isCenter: true, - ), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox.shrink(), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor) - .toShimmer2(isShow: false, radius: 12.r), - ), - ], - ) ], - ).paddingAll(16.w), + ).paddingAll(12.w), ); } } diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 0f382dc..fbe79bb 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -53,7 +53,7 @@ class MedicalFileAppointmentCard extends StatelessWidget { ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(height: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), width: 200.w, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -88,7 +88,7 @@ class MedicalFileAppointmentCard extends StatelessWidget { myAppointmentsViewModel.isMyAppointmentsLoading ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) : Expanded( - flex: 6, + flex: 7, child: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? getArrivedAppointmentButton(context).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading) : CustomButton( @@ -105,7 +105,8 @@ class MedicalFileAppointmentCard extends StatelessWidget { }, backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15), - borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), + borderColor: + AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), fontSize: 14.f, fontWeight: FontWeight.w500, @@ -166,11 +167,13 @@ class MedicalFileAppointmentCard extends StatelessWidget { return DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15 ? CustomButton( text: LocaleKeys.askDoctor.tr(context: context), - onPressed: () {}, + onPressed: () { + onAskDoctorTap(); + }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/presentation/medical_file/widgets/medical_file_card.dart b/lib/presentation/medical_file/widgets/medical_file_card.dart index 00d62c9..77968c5 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -28,19 +28,19 @@ class MedicalFileCard extends StatelessWidget { return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: backgroundColor, - borderRadius: 12.r, + borderRadius: 20.r, + hasShadow: false ), - child: Padding( - padding: EdgeInsets.all(12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Utils.buildSvgWithAssets(icon: svgIcon, width: iconS, height: iconS, fit: BoxFit.contain), - SizedBox(height: 12.h), - isLargeText ? label.toText13(color: textColor, isBold: true, maxLine: 2) : label.toText11(color: textColor, isBold: true, maxLine: 2), - ], - ), + padding: EdgeInsets.all(12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildSvgWithAssets(icon: svgIcon, width: iconS, height: iconS, fit: BoxFit.contain), + SizedBox(height: 8.h), + isLargeText ? label.toText13(color: textColor, isBold: true, maxLine: 2) : label.toText11(color: textColor, isBold: true, maxLine: 2), + ], ), ); } diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index a818ae5..6f9b8b5 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -18,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index 71abcb7..f6d7576 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -2,6 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -10,14 +13,14 @@ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_mode import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/medical_report_request_page.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/widgets/patient_medical_report_card.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'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; class MedicalReportsPage extends StatefulWidget { @@ -28,10 +31,12 @@ class MedicalReportsPage extends StatefulWidget { } class _MedicalReportsPageState extends State { + int? expandedIndex; late MedicalFileViewModel medicalFileViewModel; @override Widget build(BuildContext context) { + AppState appState = getIt.get(); medicalFileViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -45,15 +50,53 @@ class _MedicalReportsPageState extends State { return Column( children: [ SizedBox(height: 16.h), + // Status filter tabs + // Row( + // children: [ + // CustomButton( + // text: LocaleKeys.byClinic.tr(context: context), + // onPressed: () { + // medicalFileVM.setIsMedicalReportsSortByClinic(true); + // }, + // backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + // borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + // textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // SizedBox(width: 8.h), + // CustomButton( + // text: LocaleKeys.byHospital.tr(context: context), + // onPressed: () { + // medicalFileVM.setIsMedicalReportsSortByClinic(false); + // }, + // backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + // borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + // textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // ], + // ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 8.h), Row( children: [ CustomButton( text: "Requested".needTranslation, onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(0); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -65,10 +108,13 @@ class _MedicalReportsPageState extends State { CustomButton( text: LocaleKeys.ready.tr(context: context), onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(1); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -80,10 +126,13 @@ class _MedicalReportsPageState extends State { CustomButton( text: LocaleKeys.cancelled.tr(context: context), onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(2); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -93,34 +142,27 @@ class _MedicalReportsPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h), - // CustomTabBar( - // activeTextColor: Color(0xffED1C2B), - // activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - // tabs: [ - // CustomTabBarModel(null, "Requested".needTranslation), - // CustomTabBarModel(null, "Ready".needTranslation), - // CustomTabBarModel(null, "Cancelled".needTranslation), - // ], - // onTabChange: (index) { - // medicalFileViewModel.onMedicalReportTabChange(index); - // }, - // ).paddingSymmetrical(24.h, 0.h), - ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), + SizedBox(height: 12.h), + // Clinic & Hospital Sort + + // Expandable list + ListView.builder( itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading - ? 3 + ? 4 : medicalFileViewModel.patientMedicalReportList.isNotEmpty - ? medicalFileViewModel.patientMedicalReportList.length + ? medicalFileViewModel.patientMedicalReportsViewList.length : 1, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return medicalFileViewModel.isPatientMedicalReportsListLoading ? PatientMedicalReportCard( patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), medicalFileViewModel: medicalFileVM, isLoading: true, - ).paddingSymmetrical(24.h, 0.h) + ).paddingSymmetrical(0.h, 8.h) : medicalFileViewModel.patientMedicalReportList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, @@ -131,20 +173,185 @@ class _MedicalReportsPageState extends State { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: PatientMedicalReportCard( - patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index], - medicalFileViewModel: medicalFileVM, - isLoading: false, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.length} Reports Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + medicalFileVM.patientMedicalReportsViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.map((report) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + report.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: report.doctorName!.toText14(weight: FontWeight.w500)), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: report.statusDesc!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + if (medicalFileVM.selectedMedicalReportsTabIndex == 1) + Expanded( + flex: 6, + child: CustomButton( + text: "Download Report".needTranslation, + onPressed: () async { + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.getPatientMedicalReportPDF(report, appState.getAuthenticatedUser()!).then((val) async { + LoaderBottomSheet.hideLoader(); + if (medicalFileViewModel.patientMedicalReportPDFBase64.isNotEmpty) { + String path = await Utils.createFileFromString(medicalFileViewModel.patientMedicalReportPDFBase64, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + debugPrint("Error opening file: $ex"); + } + } + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.download, + iconColor: AppColors.primaryRedColor, + iconSize: 14.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), ), - ).paddingSymmetrical(24.h, 0.h), + ), ), ), ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation).paddingSymmetrical(24.h, 24.h); + : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation) + .paddingSymmetrical(24.h, 24.h); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 24.h), ], ); @@ -188,11 +395,10 @@ class _MedicalReportsPageState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 45.h, + borderRadius: 12.r, + height: 46.h, icon: AppAssets.requests, iconColor: AppColors.whiteColor, iconSize: 20.h, @@ -218,7 +424,9 @@ class _MedicalReportsPageState extends State { LoaderBottomSheet.showLoader(); await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: "Your medical report request has been successfully submitted.".needTranslation), callBackFunc: () { + showCommonBottomSheetWithoutHeight(context, + child: Utils.getSuccessWidget(loadingText: "Your medical report request has been successfully submitted.".needTranslation), + callBackFunc: () { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.onMedicalReportTabChange(0); medicalFileViewModel.getPatientMedicalReportList(); diff --git a/lib/presentation/medical_report/widgets/patient_medical_report_card.dart b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart index 1762886..413858d 100644 --- a/lib/presentation/medical_report/widgets/patient_medical_report_card.dart +++ b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -10,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/monthly_report/monthly_report.dart b/lib/presentation/monthly_report/monthly_report.dart new file mode 100644 index 0000000..1776510 --- /dev/null +++ b/lib/presentation/monthly_report/monthly_report.dart @@ -0,0 +1,189 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/cache_service.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class MonthlyReport extends StatelessWidget { + MonthlyReport({super.key}); + + late AppState appState; + final CacheService _cacheService = GetIt.instance(); + bool isTermsAccepted = true; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, monthlyReportVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.monthlyReports.tr(), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + LocaleKeys.patientHealthSummaryReport.tr(context: context).toText14(isBold: true), + const Spacer(), + Switch( + activeTrackColor: AppColors.successColor, + value: monthlyReportVM.isHealthSummaryEnabled, + onChanged: (newValue) async { + monthlyReportVM.setHealthSummaryEnabled(newValue); + }, + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "${appState.getAuthenticatedUser()!.emailAddress}".toText16(color: AppColors.textColor, weight: FontWeight.w500), + ], + ), + ], + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.prescription_remarks_icon, width: 18.w, height: 18.h), + SizedBox(width: 9.h), + Expanded( + child: + "This monthly health summary report reflects the health indicators and analysis results of the latest visits. Please note that this will be sent automatically from the system and it’s not considered as a official report so no medical decision should be taken based on it" + .needTranslation + .toText10(weight: FontWeight.w500, color: AppColors.greyTextColorLight), + ), + ], + ), + ], + ).paddingSymmetrical(24.w, 0.h), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 32.h, left: 24.w), + child: Row( + children: [ + SizedBox( + height: 24.0, + width: 24.0, + child: Checkbox( + value: isTermsAccepted, + onChanged: (v) { + isTermsAccepted = v ?? true; + }, + activeColor: AppColors.primaryRedColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ), + SizedBox(width: 10.w), + "I agree to the ".toText14(isBold: true, letterSpacing: -1.0), + "terms and conditions".toText14(isBold: true, letterSpacing: -1.0, color: AppColors.primaryRedColor, isUnderLine: true).onPress(() { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Terms.aspx', + ); + }) + ], + ), + ), + CustomButton( + text: LocaleKeys.save.tr(), + onPressed: () async { + LoaderBottomSheet.showLoader(loadingText: "Updating Monthly Report Status...".needTranslation); + await monthlyReportVM.updatePatientHealthSummaryReport( + rSummaryReport: monthlyReportVM.isHealthSummaryEnabled, + onSuccess: (response) async { + LoaderBottomSheet.hideLoader(); + await _cacheService.saveBool( + key: CacheConst.isMonthlyReportEnabled, + value: monthlyReportVM.isHealthSummaryEnabled, + ); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Monthly Report Status Updated Successfully".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onError: (error) { + // Error is already handled by errorHandlerService in view model + }, + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 46.h, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ), + ], + ); + }), + ); + } +} diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 5020b82..07f1a4f 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -7,10 +6,8 @@ import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -19,10 +16,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.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'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; -import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class FamilyMedicalScreen extends StatefulWidget { @@ -46,6 +40,9 @@ class _FamilyMedicalScreenState extends State { void initState() { super.initState(); medicalVM = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) { + medicalVM?.onFamilyFileTabChange(0); + }); } @override @@ -65,6 +62,7 @@ class _FamilyMedicalScreenState extends State { text: "Add a new family member".needTranslation, onPressed: () { DialogService dialogService = getIt.get(); + medicalVM!.clearAuthValues(); dialogService.showAddFamilyFileSheet( label: "Add Family Member".needTranslation, message: "Please fill the below field to add a new family member to your profile".needTranslation, @@ -83,13 +81,17 @@ class _FamilyMedicalScreenState extends State { children: [ appState.isChildLoggedIn ? SizedBox() - : CustomTabBar( - activeBackgroundColor: AppColors.secondaryLightRedColor, - activeTextColor: AppColors.primaryRedColor, - tabs: [CustomTabBarModel(null, LocaleKeys.family.tr()), CustomTabBarModel(null, LocaleKeys.request.tr())], - onTabChange: (index) { - medicalVM!.onFamilyFileTabChange(index); - }, + : Selector( + selector: (_, model) => model.getSelectedFamilyFileTabIndex, + builder: (context, selectedIndex, child) => CustomTabBar( + activeBackgroundColor: AppColors.secondaryLightRedColor, + activeTextColor: AppColors.primaryRedColor, + // selectedIndex: selectedIndex, + tabs: [CustomTabBarModel(null, LocaleKeys.family.tr()), CustomTabBarModel(null, LocaleKeys.request.tr())], + onTabChange: (index) { + medicalVM!.onFamilyFileTabChange(index); + }, + ), ), appState.isChildLoggedIn ? SizedBox() : SizedBox(height: 25.h), Selector(selector: (_, model) => model.getSelectedFamilyFileTabIndex, builder: (context, selectedIndex, child) => getFamilyTabs(index: selectedIndex)), @@ -117,7 +119,7 @@ class _FamilyMedicalScreenState extends State { case 1: return FamilyCards( profiles: medicalVM!.pendingFamilyFiles, - isRequestDesign: true, + isRequestDesign: medicalVM!.getSelectedFamilyFileTabIndex == 1, onSelect: (FamilyFileResponseModelLists profile) { medicalVM!.acceptRejectFileFromFamilyMembers(id: profile.id, status: 3); }, diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 2f9c871..3621cc3 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -173,12 +173,12 @@ class _FamilyCardsState extends State { children: [ Utils.buildImgWithAssets( icon: profile.gender == null - ? AppAssets.dummy_user + ? AppAssets.dummyUser : profile.gender == 1 - ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img) + ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg) : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), - width: 80.h, - height: 78.h, + width: 72.h, + height: 70.h, ), SizedBox(height: 8.h), (profile.patientName ?? "Unknown").toText14(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), @@ -186,7 +186,7 @@ class _FamilyCardsState extends State { CustomChipWidget( chipType: ChipTypeEnum.alert, backgroundColor: AppColors.lightGrayBGColor, - chipText: "Relation:${profile.relationship ?? "N/A"}", + chipText: "Relation:${profile.relationship ?? " N/A"}", iconAsset: AppAssets.heart, isShowBorder: false, borderRadius: 8.h, diff --git a/lib/presentation/my_invoices/my_invoices_details_page.dart b/lib/presentation/my_invoices/my_invoices_details_page.dart new file mode 100644 index 0000000..cccd671 --- /dev/null +++ b/lib/presentation/my_invoices/my_invoices_details_page.dart @@ -0,0 +1,272 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class MyInvoicesDetailsPage extends StatefulWidget { + GetInvoiceDetailsResponseModel getInvoiceDetailsResponseModel; + + MyInvoicesDetailsPage({super.key, required this.getInvoiceDetailsResponseModel}); + + @override + State createState() => _MyInvoicesDetailsPageState(); +} + +class _MyInvoicesDetailsPageState extends State { + late MyInvoicesViewModel myInvoicesViewModel; + + @override + Widget build(BuildContext context) { + myInvoicesViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Invoice Details".needTranslation, + sendEmail: () async { + LoaderBottomSheet.showLoader(loadingText: "Sending email, Please wait...".needTranslation); + await myInvoicesViewModel.sendInvoiceEmail( + appointmentNo: widget.getInvoiceDetailsResponseModel.appointmentNo!, + projectID: widget.getInvoiceDetailsResponseModel.projectID!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Email sent successfully.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Image.network( + widget.getInvoiceDetailsResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + ], + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.doctorNameN! : widget.getInvoiceDetailsResponseModel.doctorName!).toText16(isBold: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.invoiceNo}: ${widget.getInvoiceDetailsResponseModel.invoiceNo!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: (widget.getInvoiceDetailsResponseModel.clinicDescription!.length > 15 + ? '${widget.getInvoiceDetailsResponseModel.clinicDescription!.substring(0, 12)}...' + : widget.getInvoiceDetailsResponseModel.clinicDescription!), + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: widget.getInvoiceDetailsResponseModel.projectName!, + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.getInvoiceDetailsResponseModel.appointmentDate), false), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.getInvoiceDetailsResponseModel.listConsultation!.first.procedureName!.toText16(isBold: true), + SizedBox(height: 16.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.quantity.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.quantity!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.price.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.price!} ${LocaleKeys.sar.tr()}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.total.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.total!} ${LocaleKeys.sar.tr()}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Insurance Details".toText16(isBold: true), + SizedBox(height: 16.h), + widget.getInvoiceDetailsResponseModel.groupName!.toText14(isBold: true), + Row( + children: [ + Expanded(child: widget.getInvoiceDetailsResponseModel.companyName!.toText14(isBold: true)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + AppCustomChipWidget( + labelText: "Insurance ID: ${widget.getInvoiceDetailsResponseModel.insuranceID ?? "-"}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total Balance".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalShare.toString().toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalVATAmount!.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Discount".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.discountAmount!.toString().toText14(isBold: true, color: AppColors.primaryRedColor), + AppColors.primaryRedColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Paid".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + widget.getInvoiceDetailsResponseModel.listConsultation!.first.grandTotal!.toString().toText14(isBold: true, color: AppColors.textColor), AppColors.textColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart new file mode 100644 index 0000000..ef1a9c2 --- /dev/null +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class MyInvoicesList extends StatefulWidget { + const MyInvoicesList({super.key}); + + @override + State createState() => _MyInvoicesListState(); +} + +class _MyInvoicesListState extends State { + late MyInvoicesViewModel myInvoicesViewModel; + + @override + void initState() { + scheduleMicrotask(() { + myInvoicesViewModel.setInvoicesListLoading(); + myInvoicesViewModel.getAllInvoicesList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + myInvoicesViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: LocaleKeys.invoiceList.tr(context: context), + child: SingleChildScrollView( + child: Consumer(builder: (context, myInvoicesVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + ListView.builder( + itemCount: myInvoicesVM.isInvoicesListLoading ? 4 : myInvoicesVM.allInvoicesList.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + return myInvoicesVM.isInvoicesListLoading + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: InvoiceListCard( + getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], + onTap: () async { + myInvoicesVM.setInvoiceDetailLoading(); + LoaderBottomSheet.showLoader(loadingText: "Fetching invoice details, Please wait...".needTranslation); + await myInvoicesVM.getInvoiceDetails( + appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, + invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!, + projectID: myInvoicesVM.allInvoicesList[index].projectID!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: MyInvoicesDetailsPage(getInvoiceDetailsResponseModel: myInvoicesVM.invoiceDetailsResponseModel), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + ), + ), + ), + ), + ); + }).paddingSymmetrical(24.w, 0.h), + ], + ); + }), + ), + ); + } +} diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart new file mode 100644 index 0000000..27ca79a --- /dev/null +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class InvoiceListCard extends StatelessWidget { + final GetInvoicesListResponseModel getInvoicesListResponseModel; + Function? onTap; + + InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + icon: AppAssets.walkin_appointment_icon, + iconColor: AppColors.textColor, + labelText: 'Walk In'.needTranslation, + textColor: AppColors.textColor, + ), + AppCustomChipWidget( + labelText: 'OutPatient'.needTranslation, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + textColor: AppColors.primaryRedColor, + ), + ], + ), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Image.network( + getInvoicesListResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + Transform.translate( + offset: Offset(0.0, -20.h), + child: Container( + width: 40.w, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.circle, // Makes the container circular + border: Border.all( + color: AppColors.scaffoldBgColor, // Color of the border + width: 1.5.w, // Width of the border + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + SizedBox(height: 2.h), + "${getInvoicesListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + ], + ), + ).circle(100), + ), + ], + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (getIt().isArabic() ? getInvoicesListResponseModel.doctorNameN! : getInvoicesListResponseModel.doctorName!).toText16(isBold: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.invoiceNo}: ${getInvoicesListResponseModel.invoiceNo!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: + (getInvoicesListResponseModel.clinicName!.length > 15 ? '${getInvoicesListResponseModel.clinicName!.substring(0, 12)}...' : getInvoicesListResponseModel.clinicName!), + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: getInvoicesListResponseModel.projectName!, + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + ), + ], + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + CustomButton( + text: "View invoice details".needTranslation, + onPressed: () { + if (onTap != null) { + onTap!(); + } + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ], + ), + ), + ).paddingOnly(bottom: 16.h); + } +} diff --git a/lib/presentation/onboarding/onboarding_screen.dart b/lib/presentation/onboarding/onboarding_screen.dart index 265559b..a40a27b 100644 --- a/lib/presentation/onboarding/onboarding_screen.dart +++ b/lib/presentation/onboarding/onboarding_screen.dart @@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; @@ -30,7 +31,13 @@ class _OnboardingScreenState extends State { void goToHomePage() { Utils.saveBoolFromPrefs(CacheConst.firstLaunch, false); - Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + // Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); } @override diff --git a/lib/presentation/onboarding/splash_animation_screen.dart b/lib/presentation/onboarding/splash_animation_screen.dart index bca9681..105922b 100644 --- a/lib/presentation/onboarding/splash_animation_screen.dart +++ b/lib/presentation/onboarding/splash_animation_screen.dart @@ -1,10 +1,8 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; @@ -59,7 +57,7 @@ class _SplashAnimationScreenState extends State with Sing } } -// todo: do-not remove this code,as animation need to test on multiple screen sizes +// todo_section: do-not remove this code,as animation need to test on multiple screen sizes class AnimatedScreen extends StatefulWidget { const AnimatedScreen({super.key}); diff --git a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart new file mode 100644 index 0000000..0bedbe2 --- /dev/null +++ b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart @@ -0,0 +1,174 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget { + PrescriptionDeliveryOrderSummaryPage({super.key}); + + late PrescriptionsViewModel prescriptionsViewModel; + + @override + Widget build(BuildContext context) { + prescriptionsViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.orderSummary.tr(context: context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.orderDetail.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + ...List.generate( + prescriptionsViewModel.prescriptionDetailsList.length, + (index) => Container( + margin: EdgeInsets.all(0.0), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(5.r), + ), + child: Image.network( + prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl!, + fit: BoxFit.cover, + width: 60.w, + height: 70.h, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(8.h), + child: Center( + child: prescriptionsViewModel.prescriptionDetailsList[index].itemDescription!.trim().toText12(), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.deliveryLocation.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + ClipRRect( + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(20.r), + child: Image.network( + "https://maps.googleapis.com/maps/api/staticmap?center=${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&zoom=15&size=350x165&maptype=roadmap&markers=color:red%7C${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&key=AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng", + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: CustomButton( + text: LocaleKeys.submit.tr(context: context), + onPressed: () async { + LoaderBottomSheet.showLoader(loadingText: "Submitting your request..."); + await prescriptionsViewModel.submitPrescriptionDeliveryRequest( + latitude: prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat.toString(), + longitude: prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng.toString(), + appointmentNo: prescriptionsViewModel.prescriptionDetailsList.first.appointmentNo.toString(), + dischargeID: "0", + projectID: prescriptionsViewModel.prescriptionDetailsList.first.projectID.toString(), + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Request sent successfully.".needTranslation), + callBackFunc: () { + Navigator.of(context).pop(); + }, + title: "", + isCloseButtonVisible: true, + isDismissible: false, + isFullScreen: false, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () { + Navigator.of(context).pop(); + }, + title: "", + isCloseButtonVisible: true, + isDismissible: false, + isFullScreen: false, + ); + }); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor.withOpacity(0.01), + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.prescription_refill_icon, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart b/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart new file mode 100644 index 0000000..e2ce865 --- /dev/null +++ b/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart @@ -0,0 +1,110 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/RequestStatus.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.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/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class PrescriptionDeliveryOrdersListPage extends StatelessWidget { + const PrescriptionDeliveryOrdersListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.orders.tr(context: context), + child: SingleChildScrollView( + child: Consumer(builder: (context, model, child) { + return Column( + children: [ + ListView.builder( + itemCount: model.isPrescriptionsDeliveryOrdersLoading + ? 4 + : model.prescriptionsOrderList.isNotEmpty + ? model.prescriptionsOrderList.length + : 1, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), + itemBuilder: (context, index) { + return model.isPrescriptionsDeliveryOrdersLoading + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ) + : model.prescriptionsOrderList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + RequestStatus(status: model.prescriptionsOrderList[index].statusId ?? 0), + "Req ID: ${model.prescriptionsOrderList[index].iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Row( + spacing: 4.w, + children: [ + chip(Utils.getDayMonthYearDateFormatted(DateTime.tryParse(model.prescriptionsOrderList[index].created!)), AppAssets.calendar, + AppColors.blackBgColor), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ) + : Utils.getNoDataWidget(context, noDataText: "You don't have any prescription orders yet.".needTranslation); + }, + ).paddingSymmetrical(24.h, 0.h), + ], + ); + }), + ), + ), + ); + } + + chip(String title, String iconString, Color iconColor) { + return AppCustomChipWidget( + labelText: title, + icon: iconString, + iconColor: iconColor, + iconSize: 12.h, + ); + } +} diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index 473f79a..1216c61 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/utils/calendar_utils.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -13,23 +12,22 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/patient_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_item_view.dart'; -import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_reminder_view.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'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; class PrescriptionDetailPage extends StatefulWidget { - PrescriptionDetailPage({super.key, required this.prescriptionsResponseModel}); + PrescriptionDetailPage({super.key, required this.prescriptionsResponseModel, required this.isFromAppointments}); PatientPrescriptionsResponseModel prescriptionsResponseModel; + bool isFromAppointments = false; @override State createState() => _PrescriptionDetailPageState(); @@ -42,13 +40,14 @@ class _PrescriptionDetailPageState extends State { @override void initState() { - checkAndRemove(false); // locationUtils = new LocationUtils(isShowConfirmDialog: true, context: context); // WidgetsBinding.instance.addPostFrameCallback((_) => locationUtils.getCurrentLocation()); - scheduleMicrotask(() { - prescriptionsViewModel.setPrescriptionsDetailsLoading(); - prescriptionsViewModel.getPrescriptionDetails(widget.prescriptionsResponseModel); - }); + if (!widget.isFromAppointments) { + scheduleMicrotask(() { + prescriptionsViewModel.setPrescriptionsDetailsLoading(); + prescriptionsViewModel.getPrescriptionDetails(widget.prescriptionsResponseModel); + }); + } super.initState(); } @@ -127,7 +126,8 @@ class _PrescriptionDetailPageState extends State { children: [ AppCustomChipWidget( icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.prescriptionsResponseModel.appointmentDate), false), + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate(widget.prescriptionsResponseModel.appointmentDate), false), labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.h), ), AppCustomChipWidget( @@ -147,7 +147,7 @@ class _PrescriptionDetailPageState extends State { CustomButton( text: "Download Prescription".needTranslation, onPressed: () async { - LoaderBottomSheet.showLoader(); + LoaderBottomSheet.showLoader(loadingText: "Fetching prescription PDF, Please wait...".needTranslation); await prescriptionVM.getPrescriptionPDFBase64(widget.prescriptionsResponseModel).then((val) async { LoaderBottomSheet.hideLoader(); if (prescriptionVM.prescriptionPDFBase64Data.isNotEmpty) { @@ -169,9 +169,9 @@ class _PrescriptionDetailPageState extends State { backgroundColor: AppColors.successColor.withValues(alpha: 0.15), borderColor: AppColors.successColor.withValues(alpha: 0.01), textColor: AppColors.successColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, icon: AppAssets.download, @@ -182,8 +182,10 @@ class _PrescriptionDetailPageState extends State { ), ), ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), ListView.builder( shrinkWrap: true, + padding: EdgeInsets.zero, physics: NeverScrollableScrollPhysics(), itemCount: prescriptionVM.isPrescriptionsDetailsLoading ? 5 : prescriptionVM.prescriptionDetailsList.length, itemBuilder: (context, index) { @@ -214,18 +216,30 @@ class _PrescriptionDetailPageState extends State { hasShadow: true, ), child: CustomButton( - text: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? LocaleKeys.resendOrder.tr(context: context) : LocaleKeys.prescriptionDeliveryError.tr(context: context), - onPressed: () {}, + text: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! + ? LocaleKeys.resendOrder.tr(context: context) + : LocaleKeys.prescriptionDeliveryError.tr(context: context), + onPressed: () async { + if (widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported!) { + LoaderBottomSheet.showLoader(loadingText: "Fetching prescription details...".needTranslation); + await prescriptionsViewModel.getPrescriptionDetails(widget.prescriptionsResponseModel, onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + prescriptionsViewModel.initiatePrescriptionDelivery(); + }); + } + }, backgroundColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.greyF7Color, borderColor: AppColors.successColor.withOpacity(0.01), - textColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), + textColor: + widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), fontSize: 16, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.prescription_refill_icon, - iconColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), + iconColor: + widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), iconSize: 20.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index acf03f5..a2a3e7f 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/core/utils/calendar_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -10,6 +10,8 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; class PrescriptionItemView extends StatelessWidget { int index; @@ -154,15 +156,20 @@ class PrescriptionItemView extends StatelessWidget { activeTrackColor: AppColors.successColor.withValues(alpha: .15), value: isLoading ? false : prescriptionVM.prescriptionDetailsList[index].hasReminder!, onChanged: (newValue) async { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + if (prescriptionVM.prescriptionDetailsList[index].hasReminder ?? false) { - await checkAndRemove(prescriptionVM.prescriptionDetailsList[index].hasReminder, delete: true); - prescriptionVM.notify(); + LoaderBottomSheet.showLoader(loadingText: "Removing Reminders"); + bool resultValue = await calender.checkAndRemoveMultipleItems(id:prescriptionVM.prescriptionDetailsList[index].itemID.toString()); + + prescriptionVM.setPrescriptionItemReminder(newValue, prescriptionVM.prescriptionDetailsList[index]); + LoaderBottomSheet.hideLoader(); return; } DateTime startDate = DateTime.now(); DateTime endDate = DateTime(startDate.year, startDate.month, startDate.day + prescriptionVM.prescriptionDetailsList[index].days!.toInt()); - showReminderBottomSheet( + BottomSheetUtils().showReminderBottomSheet( context, endDate, "", @@ -173,18 +180,29 @@ class PrescriptionItemView extends StatelessWidget { description: "${prescriptionVM.prescriptionDetailsList[index].itemDescription} ${prescriptionVM.prescriptionDetailsList[index].frequency} ${prescriptionVM.prescriptionDetailsList[index].route} ", onSuccess: () { - prescriptionVM.setPrescriptionItemReminder(newValue, prescriptionVM.prescriptionDetailsList[index]); + }, isMultiAllowed: true, - onMultiDateSuccess: (int selectedIndex) { - setCalender(context, - eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - selectedMinutes: selectedIndex, + onMultiDateSuccess: (int selectedIndex) async{ + bool isEventAdded = await calender.createMultipleEvents( + reminderMinutes: selectedIndex, frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), - days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, - route: prescriptionVM.prescriptionDetailsList[index].route!); + route: prescriptionVM.prescriptionDetailsList[index].route!, + onFailure: (errorMessage)=> prescriptionVM.showError(errorMessage), + prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + ); + prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); + // setCalender(context, + // eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + // selectedMinutes: selectedIndex, + // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), + // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, + // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, + // route: prescriptionVM.prescriptionDetailsList[index].route!); }, ); }, @@ -202,9 +220,9 @@ class PrescriptionItemView extends StatelessWidget { backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), borderColor: AppColors.primaryRedColor.withOpacity(0.0), textColor: AppColors.primaryRedColor, - fontSize: 13, + fontSize: 14.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ).toShimmer2(isShow: isLoading), diff --git a/lib/presentation/prescriptions/prescription_reminder_view.dart b/lib/presentation/prescriptions/prescription_reminder_view.dart index 8b7df0b..2f1154f 100644 --- a/lib/presentation/prescriptions/prescription_reminder_view.dart +++ b/lib/presentation/prescriptions/prescription_reminder_view.dart @@ -100,7 +100,7 @@ class _PrescriptionReminderViewState extends State { text: LocaleKeys.setReminder.tr(), onPressed: () { Navigator.of(context).pop(); - widget.setReminder(_selectedOption); + widget.setReminder(_options[_selectedOption]); }, backgroundColor: AppColors.bgGreenColor, borderColor: AppColors.bgGreenColor, diff --git a/lib/presentation/prescriptions/prescriptions_list_page.dart b/lib/presentation/prescriptions/prescriptions_list_page.dart index 1293c9c..8b60159 100644 --- a/lib/presentation/prescriptions/prescriptions_list_page.dart +++ b/lib/presentation/prescriptions/prescriptions_list_page.dart @@ -14,13 +14,13 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_delivery_orders_list_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class PrescriptionsListPage extends StatefulWidget { @@ -51,6 +51,14 @@ class _PrescriptionsListPageState extends State { backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.prescriptions.tr(context: context), + requests: () { + prescriptionsViewModel.getPrescriptionOrdersList(); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDeliveryOrdersListPage(), + ), + ); + }, child: SingleChildScrollView( child: Consumer(builder: (context, model, child) { return Column( @@ -118,190 +126,201 @@ class _PrescriptionsListPageState extends State { child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: InkWell( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomButton( - text: "${model.patientPrescriptionOrdersViewList[index].prescriptionsList!.length} Prescriptions Available", - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${model.patientPrescriptionOrdersViewList[index].prescriptionsList!.length} Prescriptions Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], ), - Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + SizedBox(height: 8.h), + model.patientPrescriptionOrdersViewList[index].filterName!.toText16(isBold: true) ], ), - SizedBox(height: 8.h), - model.patientPrescriptionOrdersViewList[index].filterName!.toText16(isBold: true) - ], - ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 500), - switchInCurve: Curves.easeIn, - switchOutCurve: Curves.easeOut, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: 0.0, - child: child, - ), - ); - }, - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...model.patientPrescriptionOrdersViewList[index].prescriptionsList!.map((prescription) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.network( - prescription.doctorImageURL!, - width: 24.h, - height: 24.h, - fit: BoxFit.fill, - ).circle(100), - SizedBox(width: 8.h), - Expanded(child: prescription.doctorName!.toText14(weight: FontWeight.w500)), - ], - ), - SizedBox(height: 8.h), - Row( + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...model.patientPrescriptionOrdersViewList[index].prescriptionsList!.map((prescription) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomButton( - text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(prescription.appointmentDate), false), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + prescription.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: prescription.doctorName!.toText14(weight: FontWeight.w500)), + ], ), - SizedBox(width: 8.h), - CustomButton( - text: model.isSortByClinic ? prescription.name! : prescription.clinicDescription!, - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, - ), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Expanded( - flex: 6, - child: CustomButton( - text: prescription.isHomeMedicineDeliverySupported! - ? LocaleKeys.resendOrder.tr(context: context) - : LocaleKeys.prescriptionDeliveryError.tr(context: context), - onPressed: () {}, - backgroundColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor.withOpacity(0.15) : AppColors.greyF7Color, - borderColor: AppColors.successColor.withOpacity(0.01), - textColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), - fontSize: prescription.isHomeMedicineDeliverySupported! ? 14 : 12, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - icon: AppAssets.prescription_refill_icon, - iconColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), - iconSize: 14.h, - ), + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(prescription.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: model.isSortByClinic ? prescription.name! : prescription.clinicDescription!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( - height: 40.h, - width: 40.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 10.h, + SizedBox(height: 8.h), + Row( + children: [ + Expanded( + flex: 6, + child: CustomButton( + text: prescription.isHomeMedicineDeliverySupported! + ? LocaleKeys.resendOrder.tr(context: context) + : LocaleKeys.prescriptionDeliveryError.tr(context: context), + onPressed: () async { + if (prescription.isHomeMedicineDeliverySupported!) { + LoaderBottomSheet.showLoader(loadingText: "Fetching prescription details...".needTranslation); + await prescriptionsViewModel.getPrescriptionDetails(prescriptionsViewModel.patientPrescriptionOrders[index], + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + prescriptionsViewModel.initiatePrescriptionDelivery(); + }); + } + }, + backgroundColor: + prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor.withOpacity(0.15) : AppColors.greyF7Color, + borderColor: AppColors.successColor.withOpacity(0.01), + textColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), + fontSize: prescription.isHomeMedicineDeliverySupported! ? 14.f : 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.prescription_refill_icon, + iconColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), + iconSize: 16.h, + ), ), - child: Padding( - padding: EdgeInsets.all(8.h), - child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, - width: 10.h, - height: 10.h, - fit: BoxFit.contain, + SizedBox(width: 8.h), + Expanded( + flex: 1, + child: Container( + height: 40.h, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.textColor, + borderRadius: 12, ), - ), + child: Padding( + padding: EdgeInsets.all(12.h), + child: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.whiteColor, + fit: BoxFit.contain, + ), + ), + ), + ).onPress(() { + model.setPrescriptionsDetailsLoading(); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage( + prescriptionsResponseModel: prescription, + isFromAppointments: false, + ), + ), + ); + }), ), - ).onPress(() { - model.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage(prescriptionsResponseModel: prescription), - ), - ); - }), + ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - SizedBox(height: 12.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), - SizedBox(height: 12.h), - ], - ); - }).toList(), - ], - ), - ) - : SizedBox.shrink(), + ); + }).toList(), + ], + ), + ) + : SizedBox.shrink(), + ), + ], ), - ], + ), ), ), ), - ), - ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any prescriptions yet.".needTranslation); }, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index f5e2ef8..e463ae7 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; @@ -28,19 +30,16 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; - -import '../../core/dependencies.dart' show getIt; +import 'package:url_launcher/url_launcher.dart'; class ProfileSettings extends StatefulWidget { - ProfileSettings({Key? key}) : super(key: key); + const ProfileSettings({super.key}); @override - _ProfileSettingsState createState() { - return _ProfileSettingsState(); - } + ProfileSettingsState createState() => ProfileSettingsState(); } -class _ProfileSettingsState extends State { +class ProfileSettingsState extends State { @override void initState() { super.initState(); @@ -237,15 +236,35 @@ class _ProfileSettingsState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Column( children: [ - actionItem(AppAssets.call_fill, "Contact Us".needTranslation, () {}, trailingLabel: "9200666666"), + actionItem(AppAssets.call_fill, "Contact Us".needTranslation, () { + launchUrl(Uri.parse("tel://" + "+966 11 525 9999")); + }, trailingLabel: "011 525 9999"), 1.divider, actionItem(AppAssets.permission, "Permissions".needTranslation, () {}, trailingLabel: "Location, Camera"), 1.divider, - actionItem(AppAssets.rate, "Rate Our App".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.rate, "Rate Our App".needTranslation, () { + if (Platform.isAndroid) { + Utils.openWebView( + url: 'https://play.google.com/store/apps/details?id=com.ejada.hmg', + ); + } else { + Utils.openWebView( + url: 'https://itunes.apple.com/app/id733503978', + ); + } + }, isExternalLink: true), 1.divider, - actionItem(AppAssets.privacy_terms, "Privacy Policy".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.privacy_terms, "Privacy Policy".needTranslation, () { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Privacy.aspx', + ); + }, isExternalLink: true), 1.divider, - actionItem(AppAssets.privacy_terms, "Terms & Conditions".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.privacy_terms, "Terms & Conditions".needTranslation, () { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Terms.aspx', + ); + }, isExternalLink: true), ], ), ), @@ -306,8 +325,8 @@ class FamilyCardWidget extends StatelessWidget { Widget build(BuildContext context) { AppState appState = getIt.get(); final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); - final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; - final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); + // final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + // final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -323,7 +342,7 @@ class FamilyCardWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(profile.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.w, height: 56.h), + Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart new file mode 100644 index 0000000..eaee4c0 --- /dev/null +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -0,0 +1,207 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class FamilyCardWidget extends StatelessWidget { + final Function() onAddFamilyMemberPress; + final Function(FamilyFileResponseModelLists member) onFamilySwitchPress; + final FamilyFileResponseModelLists profile; + + const FamilyCardWidget({ + super.key, + required this.onAddFamilyMemberPress, + required this.profile, + required this.onFamilySwitchPress(FamilyFileResponseModelLists member), + }); + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); + final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + "${profile.patientName}".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + AppCustomChipWidget( + icon: AppAssets.file_icon, + labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${profile.responseId}", + iconSize: 12.w, + ), + ], + ).expanded, + Icon(Icons.qr_code, size: 56.h) + ], + ), + SizedBox(height: 4.h), + SizedBox( + child: Wrap( + alignment: WrapAlignment.start, + spacing: 4.w, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "${profile.age} Years Old".needTranslation, + ), + isActive && appState.getAuthenticatedUser()!.bloodGroup != null + ? AppCustomChipWidget( + icon: AppAssets.blood_icon, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), + labelText: "Blood: ${appState.getAuthenticatedUser()!.bloodGroup ?? ""}", + iconColor: AppColors.primaryRedColor) + : SizedBox(), + Selector( + selector: (context, insuranceVM) => ( + isEmpty: insuranceVM.patientInsuranceList.isEmpty, + patientID: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.patientID : null, + isLoading: insuranceVM.isInsuranceLoading, + cardValidTo: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.cardValidTo : null + ), + builder: (context, data, child) { + if (data.isEmpty) { + return const SizedBox(); + } else if (profile.responseId != data.patientID) { + return SizedBox(); + } + + final isLoading = data.isLoading; + final isExpired = !isLoading && DateTime.now().isAfter(DateUtil.convertStringToDate(data.cardValidTo)); + + final String icon; + final String labelText; + final Color iconColor; + final Color backgroundColor; + + if (isLoading) { + icon = AppAssets.cancel_circle_icon; + labelText = "Insurance".needTranslation; + iconColor = AppColors.primaryRedColor; + backgroundColor = AppColors.primaryRedColor; + } else if (isExpired) { + icon = AppAssets.cancel_circle_icon; + labelText = "Insurance Expired".needTranslation; + iconColor = AppColors.primaryRedColor; + backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); + } else { + icon = AppAssets.insurance_active_icon; + labelText = "Insurance Active".needTranslation; + iconColor = AppColors.successColor; + backgroundColor = AppColors.successColor.withValues(alpha: 0.15); + } + + return AppCustomChipWidget( + icon: icon, + labelText: labelText, + iconColor: iconColor, + iconSize: 12.w, + backgroundColor: backgroundColor, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), + // padding: EdgeInsets.zero, + ).toShimmer2(isShow: isLoading); + }, + ) + ], + ), + ), + ], + ).paddingOnly(top: 16.h, right: 16.w, left: 16.w, bottom: 12.h), + 1.divider, + _buildActionButton(appState), + ], + ), + ); + } + + Widget _buildActionButton(AppState appState) { + final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + final int? currentUserId = appState.getAuthenticatedUser()?.patientId; + final int? superUserId = appState.getSuperUserID; + + if (isParentUser) { + return _buildParentUserButton(currentUserId); + } else { + return _buildNonParentUserButton(superUserId); + } + } + + Widget _buildParentUserButton(int? currentUserId) { + final canSwitch = profile.responseId != currentUserId; + + return CustomButton( + icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family, + text: canSwitch ? "Switch Family File".needTranslation : "Add a new family member".needTranslation, + onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress, + backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, + borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, + textColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, + iconColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, + height: isFoldable ? 50.h : 40.h, + fontSize: 14.f, + ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); + } + + Widget _buildNonParentUserButton(int? superUserId) { + final canSwitchBack = superUserId != null && superUserId == profile.responseId; + + return CustomButton( + icon: AppAssets.switch_user, + text: canSwitchBack ? "Switch Back To Family File".needTranslation : "Switch".needTranslation, + backgroundColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, + borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, + textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, + iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, + onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {}, + height: isFoldable ? 50.h : 40.h, + fontSize: 14.f, + ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); + } + +// //TODO: Add family file switch logic here +// isRootUser +// ? CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {}) +// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16) +// : CustomButton( +// icon: AppAssets.add_family, +// backgroundColor: AppColors.secondaryLightRedColor, +// borderColor: AppColors.secondaryLightRedColor, +// textColor: AppColors.primaryRedColor, +// iconColor: AppColors.primaryRedColor, +// text: "Switch to this medical file".needTranslation, +// height: 40.h, +// fontSize: 14, +// onPressed: () {}) +// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16), +// +} diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index 6662a8e..fb153ea 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -3,25 +3,26 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart'; +import 'package:hmg_patient_app_new/presentation/radiology/search_radiology.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'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; import '../../features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; class RadiologyOrdersPage extends StatefulWidget { const RadiologyOrdersPage({super.key}); @@ -32,183 +33,345 @@ class RadiologyOrdersPage extends StatefulWidget { class _RadiologyOrdersPageState extends State { late RadiologyViewModel radiologyViewModel; - + String selectedFilterText = ''; int? expandedIndex; + // Scroll controller to ensure expanded group is visible + late ScrollController _scrollController; + final Map _groupKeys = {}; @override void initState() { + _scrollController = ScrollController(); scheduleMicrotask(() { - radiologyViewModel.initRadiologyProvider(); + radiologyViewModel.initRadiologyViewModel(); }); super.initState(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { radiologyViewModel = Provider.of(context); return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.radiology.tr(context: context), - child: SingleChildScrollView( - child: Consumer( - builder: (context, model, child) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Expandable list - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: model.isRadiologyOrdersLoading - ? 5 - : model.patientRadiologyOrders.isNotEmpty - ? model.patientRadiologyOrders.length - : 1, - itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; - return model.isRadiologyOrdersLoading - ? LabResultItemView( - onTap: () {}, - labOrder: null, - index: index, - isLoading: true, - ) - : model.patientRadiologyOrders.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: InkWell( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppCustomChipWidget( - labelText: LocaleKeys.resultsAvailable.tr(context: context), - backgroundColor: AppColors.successColor.withOpacity(0.15), - textColor: AppColors.successColor, - ).toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 100), - SizedBox(height: 8.h), - Row( - children: [ - Image.network( - model.isRadiologyOrdersLoading - ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" - : model.patientRadiologyOrders[index].doctorImageURL!, - width: 24.h, - height: 24.h, - fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: model.isRadiologyOrdersLoading), - SizedBox(width: 4.h), - (model.isRadiologyOrdersLoading ? "Dr John Smith" : model.patientRadiologyOrders[index].doctorName!) - .toText16(isBold: true) - .toShimmer2(isShow: model.isRadiologyOrdersLoading) - ], - ), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: model.isRadiologyOrdersLoading ? "01 Jan 2025" : DateUtil.formatDateToDate(model.patientRadiologyOrders[index].orderDate!, false), - ).toShimmer2(isShow: model.isRadiologyOrdersLoading), - AppCustomChipWidget( - labelText: model.isRadiologyOrdersLoading ? "01 Jan 2025" : model.patientRadiologyOrders[index].clinicDescription!, - ).toShimmer2(isShow: model.isRadiologyOrdersLoading), + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.radiology.tr(context: context), + search: () async { + final lavVM = Provider.of(context, listen: false); + if (lavVM.isLabOrdersLoading) { + return; + } else { + String? value = await Navigator.of(context).push( + CustomPageRoute( + page: SearchRadiologyContent(radiologySuggestionsList: radiologyViewModel.radiologySuggestions), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + if (value != null) { + selectedFilterText = value; + radiologyViewModel.filterRadiologyReports(value); + } + } + }, + child: SingleChildScrollView( + controller: _scrollController, + physics: NeverScrollableScrollPhysics(), + child: Consumer( + builder: (context, model, child) { + // Build grouping lists if we have data and none constructed yet + if (!model.isRadiologyOrdersLoading && model.patientRadiologyOrders.isNotEmpty && model.patientRadiologyOrdersViewList.isEmpty) { + final clinicMap = >{}; + final hospitalMap = >{}; + for (var order in model.patientRadiologyOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + model.patientRadiologyOrdersByClinic = clinicMap.values.toList(); + model.patientRadiologyOrdersByHospital = hospitalMap.values.toList(); + model.patientRadiologyOrdersViewList = model.isSortByClinic ? model.patientRadiologyOrdersByClinic : model.patientRadiologyOrdersByHospital; + } - // AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), - // AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), - ], - ), - ], - ), + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Clinic / Hospital toggle + SizedBox(height: 16.h), + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + model.setIsSortByClinic(true); + }, + backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + model.setIsSortByClinic(false); + }, + backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ), + + SizedBox(height: 8.h), + selectedFilterText.isNotEmpty + ? AppCustomChipWidget( + padding: EdgeInsets.symmetric(horizontal: 5.h), + labelText: selectedFilterText, + deleteIcon: 'assets/images/svg/cross_circle.svg', + backgroundColor: AppColors.alertColor, + textColor: AppColors.whiteColor, + deleteIconColor: AppColors.whiteColor, + deleteIconHasColor: true, + onDeleteTap: () { + setState(() { + selectedFilterText = ''; + model.filterRadiologyReports(''); + }); + }, + // chipType: ChipTypeEnum.alert, + // isSelected: true, + ) + : SizedBox(), + + // Grouped view when available + Builder(builder: (ctx) { + if (model.isRadiologyOrdersLoading) { + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: 5, + itemBuilder: (context, index) => LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ), + ); + } + + if (model.patientRadiologyOrdersViewList.isEmpty) { + return Utils.getNoDataWidget(ctx, noDataText: "You don't have any radiology results yet.".needTranslation); + } + + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: model.patientRadiologyOrdersViewList.length, + itemBuilder: (context, index) { + final group = model.patientRadiologyOrdersViewList[index]; + final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); + final isExpanded = expandedIndex == index; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 400), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.0, + ); + } + }); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], ), - model.isRadiologyOrdersLoading - ? SizedBox.shrink() - : AnimatedCrossFade( - firstChild: SizedBox.shrink(), - secondChild: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: '● ${model.patientRadiologyOrders[index].description}'.toText14(weight: FontWeight.w500), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((order) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(), - CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewReport.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: RadiologyResultPage(patientRadiologyResponseModel: model.patientRadiologyOrders[index]), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 24.w, + height: 24.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Expanded( + child: (order.doctorName ?? '').toString().toText14(weight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + if ((order.description ?? '').isNotEmpty) + AppCustomChipWidget( + labelText: (order.description ?? '').toString(), ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14, - fontWeight: FontWeight.bold, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, + AppCustomChipWidget( + labelText: DateUtil.formatDateToDate( + (order.orderDate ?? order.appointmentDate), + false, + ), + ), + AppCustomChipWidget( + labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''), + ), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded(flex: 2, child: const SizedBox()), + Expanded( + flex: 2, + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: "View Results".needTranslation, + onPressed: () { + model.navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: order), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ), + ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - ], - ), + ); + }).toList(), + ], ), - crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: Duration(milliseconds: 300), - ), - ], - ), + ) + : const SizedBox.shrink(), + ), + ], ), ), ), ), - ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any radiology results yet.".needTranslation); - }, - ), - ], - ), - ); - }, + ), + ); + }, + ); + }), + ], + ), + ); + }, + ), ), ), - ), - ); + ); } Color getLabOrderStatusColor(num status) { diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index df34164..1fc2d9f 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -10,7 +10,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; diff --git a/lib/presentation/radiology/search_radiology.dart b/lib/presentation/radiology/search_radiology.dart new file mode 100644 index 0000000..8d63a59 --- /dev/null +++ b/lib/presentation/radiology/search_radiology.dart @@ -0,0 +1,153 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; + +class SearchRadiologyContent extends StatefulWidget { + final List radiologySuggestionsList; + + const SearchRadiologyContent({super.key, required this.radiologySuggestionsList}); + + @override + State createState() => _SearchRadiologyContentContentState(); +} + +class _SearchRadiologyContentContentState extends State { + TextEditingController searchEditingController = TextEditingController(); + List filteredSuggestions = []; + + @override + void initState() { + super.initState(); + filteredSuggestions = List.from(widget.radiologySuggestionsList); + + // Listen for changes in the search field + searchEditingController.addListener(() { + filterSuggestions(); + }); + } + + @override + void dispose() { + searchEditingController.dispose(); + super.dispose(); + } + + void filterSuggestions() { + final query = searchEditingController.text.toLowerCase(); + + if (query.isEmpty) { + setState(() { + filteredSuggestions = List.from(widget.radiologySuggestionsList); + }); + } else { + setState(() { + filteredSuggestions = widget.radiologySuggestionsList.where((suggestion) => suggestion.toLowerCase().contains(query)).toList(); + }); + } + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: LocaleKeys.radiology.tr(), + isClose: true, + bottomChild: Container( + color: Colors.white, + padding: EdgeInsets.all(ResponsiveExtension(20).h), + child: CustomButton( + text: LocaleKeys.search.tr(), + icon: AppAssets.search_icon, + iconColor: Colors.white, + onPressed: () => Navigator.pop(context, searchEditingController.text), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, top: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextInputWidget( + labelText: "Search radiology results", + hintText: "Type test description", + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(9).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + SizedBox(height: ResponsiveExtension(20).h), + if (filteredSuggestions.isNotEmpty) ...[ + "Suggestions".toText16(isBold: true), + ], + ], + ), + ), + SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 16), + child: Wrap( + alignment: WrapAlignment.start, + spacing: 10, + runSpacing: 10, + children: filteredSuggestions + .map((label) => SuggestionChip( + label: label, + onTap: () { + searchEditingController.text = label; + }, + )) + .toList(), + ), + ), + ], + ), + ); + } +} + +class SuggestionChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback? onTap; + + const SuggestionChip({ + super.key, + required this.label, + this.isSelected = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor, + borderRadius: BorderRadius.circular(8), + ), + child: label.toText12( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart new file mode 100644 index 0000000..5fb1fa3 --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentClinic extends StatefulWidget { + + late final String? doctorNote; + late final int? doctorRate; + + RateAppointmentClinic({this.doctorRate, this.doctorNote}); + + @override + _RateAppointmentClinicState createState() => _RateAppointmentClinicState(); +} + +class _RateAppointmentClinicState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + // Make the sheet a fixed height and keep content scrollable while pinning buttons to bottom + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable content + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: true, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + ), + ), + SizedBox(height: 16), + + // Rate clinic box + SizedBox( + width: double.infinity, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Rate Clinic".needTranslation.toText16(isBold: true), + + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => rating == (index + 1) + ? Container( + margin: EdgeInsets.only(left: 3.0, right: 3.0), + child: IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg', colorFilter: getColors(rating)), + ), + ) + : IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + SizedBox(height: 12), + + // Extra content area (keeps any other widgets that were previously below) + Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Placeholder for in-content widgets if needed in future + ], + ), + ), + + // Add bottom spacing so last content isn't obscured by the fixed buttons + SizedBox(height: 12), + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom of the sheet + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric( vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Back".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + appointmentRatingViewModel!.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your last visit with doctor?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(false); + setState(() { + + }); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Submit".needTranslation, + onPressed: () { + + submitRating(); + + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + ColorFilter getColors(int rating){ + + switch(rating){ + case 5: + return ColorFilter.mode(AppColors.bgGreenColor, BlendMode.srcIn); + case 4: + return ColorFilter.mode(Colors.greenAccent, BlendMode.srcIn); + case 3: + return ColorFilter.mode(AppColors.warningLightColor, BlendMode.srcIn); + case 2: + return ColorFilter.mode(Colors.orange, BlendMode.srcIn); + case 1: + return ColorFilter.mode(AppColors.primaryRedColor, BlendMode.srcIn); + + default: + return ColorFilter.mode(AppColors.greyColor, BlendMode.srcIn); + } + } + + submitRating() async{ + LoaderBottomSheet.showLoader(); + await appointmentRatingViewModel!.submitDoctorRating(docRate: widget.doctorRate!, docNote: widget.doctorNote!); + await appointmentRatingViewModel!.submitClinicRating(clinicRate: rating, clinicNote: note); + LoaderBottomSheet.hideLoader(); + Navigator.pop(context); + } + +} diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart new file mode 100644 index 0000000..ac79744 --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_clinic.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentDoctor extends StatefulWidget { + + bool isFromRegistration; + + RateAppointmentDoctor({Key? key, this.isFromRegistration = false}) : super(key: key); + + @override + _RateAppointmentDoctorState createState() => _RateAppointmentDoctorState(); +} + +class _RateAppointmentDoctorState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + + // ProjectViewModel? projectViewModel; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + void initState() { + + super.initState(); + } + + + @override + Widget build(BuildContext context) { + + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return Selector( + selector: (_, vm) => vm.isRateClinic, + builder: (context, isRateClinic, child) => isRateClinic + ? RateAppointmentClinic(doctorNote: note, doctorRate: rating,) + : SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable main content + Expanded( + + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: false, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + )), + + SizedBox(height: 16), + + // Rating box + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Please rate the doctor".needTranslation.toText16(isBold: true), + + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => AnimatedSwitcher( + duration: Duration(milliseconds: 1000), + switchInCurve: Curves.elasticOut, + switchOutCurve: Curves.elasticIn, + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(child: child, scale: animation); + }, + child: Container( + key: ValueKey(rating), + child: IconButton( + iconSize: 45.0, + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + color: rating >= (index + 1) + ? Color.fromRGBO(255, 186, 0, 1.0) + : Colors.grey[400], + icon: Icon(rating >= (index + 1) ? Icons.star : Icons.star)), + ), + ), + ) + ], + ), + ], + ), + ), + ), + + SizedBox(height: 12), + + // Note text field + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.0), + child: TextField( + + maxLines: 4, + decoration: InputDecoration.collapsed( + hintText: "Notes".needTranslation, + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xff2B353E), + letterSpacing: -0.64, + height: 23 / 16)), + onChanged: (value) { + setState(() { + note = value; + }); + }, + ))), + + + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Later".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () { + // Set up clinic rating and show clinic rating view + appointmentRatingViewModel!.setTitle("Rate Clinic".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your appointment?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(true); + + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ], + ), + )); + + // DoctorList getDoctorObject(AppointmentRateViewModel model) { + // DoctorList doctor = new DoctorList(); + // + // doctor.name = model.appointmentDetails.doctorName; + // doctor.doctorImageURL = model.appointmentDetails.doctorImageURL; + // doctor.clinicName = model.appointmentDetails.clinicName; + // doctor.projectName = model.appointmentDetails.projectName; + // doctor.date = model.appointmentDetails.appointmentDate; + // doctor.actualDoctorRate = 5; + // + // return doctor; + // } + } + + +} diff --git a/lib/presentation/rate_appointment/widget/doctor_row.dart b/lib/presentation/rate_appointment/widget/doctor_row.dart new file mode 100644 index 0000000..725ac7c --- /dev/null +++ b/lib/presentation/rate_appointment/widget/doctor_row.dart @@ -0,0 +1,94 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class BuildDoctorRow extends StatelessWidget { + bool isForClinic = false; + AppointmentDetails? appointmentDetails; + + BuildDoctorRow({super.key, required this.isForClinic, this.appointmentDetails}); + + @override + Widget build(BuildContext context) { + + return Padding(padding: EdgeInsets.all(16),child:Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL , + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (isForClinic ? appointmentDetails!.projectName : appointmentDetails!.doctorName)!.toString() + .toText16(isBold: true, maxlines: 1), + + SizedBox(height: 8.h), + + + isForClinic ? Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ), + AppCustomChipWidget( + icon: AppAssets.ic_date_filter, + labelText: + DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointmentDetails!.appointmentDate), false), + + ), + + AppCustomChipWidget( + icon: AppAssets.appointment_time_icon, + labelText: + appointmentDetails!.startTime.substring(0, appointmentDetails!.startTime.length - 3), + + ), + + ] + ) : Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.projectName.toString(), + + + ), + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ) + + ] + ) + ], + ), + ), + ], + )); + } + +} \ No newline at end of file diff --git a/lib/presentation/services/services_page.dart b/lib/presentation/services/services_page.dart deleted file mode 100644 index 24a259b..0000000 --- a/lib/presentation/services/services_page.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; - -class ServicesPage extends StatelessWidget { - const ServicesPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - appBar: AppBar( - title: const Text('Appointments'), - backgroundColor: AppColors.bgScaffoldColor, - ), - body: const Center( - child: Text( - 'Appointments Page', - style: TextStyle(fontSize: 24), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart new file mode 100644 index 0000000..7c537ba --- /dev/null +++ b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart @@ -0,0 +1,419 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:provider/provider.dart'; +import 'package:health/health.dart'; + +class HealthDashboard extends StatefulWidget { + const HealthDashboard({Key? key}) : super(key: key); + + @override + _HealthDashboardState createState() => _HealthDashboardState(); +} + +class _HealthDashboardState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + final dateFormat = DateFormat('MMM dd, yyyy'); + final timeFormat = DateFormat('hh:mm a'); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + Future.microtask( + () async { + await Health().configure(); + context.read().fetchHealthData(); + }, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, healthProvider, child) { + return SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 96.h), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "Overview".needTranslation), + CustomTabBarModel(null, "Details".needTranslation), + ], + onTabChange: (index) { + healthProvider.onTabChanged(index); + }, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + getSelectedTabData(healthProvider.selectedTabIndex, healthProvider).paddingSymmetrical(24.w, 0), + ], + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.read().fetchHealthData(), + tooltip: 'Refresh health data', + child: Icon(Icons.refresh), + ), + ); + } + + Widget getSelectedTabData(int index, HealthProvider healthProvider) { + switch (index) { + case 0: + return _buildOverviewTab(healthProvider); + case 1: + return _buildDetailsTab(healthProvider); + default: + SizedBox.shrink(); + } + return Container(); + } + + Widget _buildOverviewTab(HealthProvider healthProvider) { + if (healthProvider.isLoading) { + return _buildLoadingState(); + } + + if (healthProvider.error != null) { + return _buildErrorState(healthProvider.error!); + } + return CustomScrollView( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: _buildTimeRangeSelector(healthProvider), + ), + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 12.w), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 0.62, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final metric = HealthMetrics.metrics[index]; + return HealthMetricCard( + metric: metric, + data: healthProvider.healthData[metric.type] ?? [], + onTap: () => _showMetricDetails(context, metric, healthProvider), + ); + }, + childCount: HealthMetrics.metrics.length, + ), + ), + ), + ], + ); + } + + Widget _buildDetailsTab(HealthProvider healthProvider) { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: HealthMetrics.metrics.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final metric = HealthMetrics.metrics[index]; + final data = healthProvider.healthData[metric.type] ?? []; + + return Card( + color: AppColors.whiteColor, + margin: EdgeInsets.only(bottom: 16), + child: ExpansionTile( + leading: Icon( + metric.icon, + color: metric.color, + size: 32.h, + ), + // title: Text(metric.name), + title: (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText14(isBold: true), + subtitle: Text( + data.isEmpty ? LocaleKeys.noDataAvailable.tr(context: context) : '${_getValueAsDouble(data.last.value, metric.type).toStringAsFixed(1)} ${metric.unit}', + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + metric.description.toText13(), + SizedBox(height: 16.h), + if (data.isNotEmpty) ...[ + Text( + 'History', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + SizedBox(height: 8), + ...data.reversed + .take(5) + .map((point) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + dateFormat.format(point.dateFrom), + style: TextStyle(color: Colors.grey[600]), + ), + Text( + '${_getValueAsDouble(point.value, metric.type).toStringAsFixed(1)} ${metric.unit}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + )) + .toList(), + ], + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildTimeRangeSelector(HealthProvider provider) { + return Container( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (final range in ['1D', '7D', '1M', '3M', '1Y']) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(range), + labelStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + checkmarkColor: AppColors.primaryRedColor, + selected: provider.selectedTimeRange == range, + selectedColor: AppColors.secondaryLightRedColor, + onSelected: (selected) { + if (selected) { + provider.updateTimeRange(range); + } + }, + ), + ), + ], + ), + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading health data...'), + ], + ), + ); + } + + Widget _buildErrorState(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Text( + 'Error loading health data', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + error, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[600]), + ), + SizedBox(height: 24), + ElevatedButton( + onPressed: () => context.read().fetchHealthData(), + child: Text('Try Again'), + ), + ], + ), + ); + } + + void _showMetricDetails( + BuildContext context, + HealthMetricInfo metric, + HealthProvider provider, + ) { + final data = provider.healthData[metric.type] ?? []; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) => Container( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + children: [ + Icon(metric.icon, color: metric.color, size: 30), + SizedBox(width: 16), + (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText24(isBold: true), + // Text( + // metric.name, + // style: TextStyle( + // fontSize: 24, + // fontWeight: FontWeight.bold, + // ), + // ), + ], + ), + SizedBox(height: 16), + Text( + metric.description, + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + SizedBox(height: 24), + Text( + 'Healthy Range', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + '${metric.minHealthyValue} - ${metric.maxHealthyValue} ${metric.unit}'.toText16(isBold: true, color: AppColors.primaryRedColor), + // Text( + // '${metric.minHealthyValue} - ${metric.maxHealthyValue} ${metric.unit}', + // style: TextStyle( + // color: mainPurple, + // fontSize: 16, + // ), + // ), + SizedBox(height: 24), + if (data.isNotEmpty) ...[ + Text( + 'History', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 16), + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: data.length, + itemBuilder: (context, index) { + final point = data[data.length - 1 - index]; + final value = _getValueAsDouble(point.value, metric.type); + return Card( + child: ListTile( + title: Text( + '${value.toStringAsFixed(1)} ${metric.unit}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + // dateFormat.format(point.dateFrom), + Utils.getMonthDayYearDateFormatted(point.dateFrom) + // dateFormat.format(point.dateFrom), + ), + trailing: Icon( + value >= metric.minHealthyValue && value <= metric.maxHealthyValue ? Icons.check_circle : Icons.warning, + color: value >= metric.minHealthyValue && value <= metric.maxHealthyValue ? AppColors.textColor : Colors.orange, + ), + ), + ); + }, + ), + ), + ] else + Center( + child: Text( + 'No data available', + style: TextStyle(color: Colors.grey[600]), + ), + ), + ], + ), + ), + ), + ); + } + + // Add this helper method to the _HealthDashboardState class + double _getValueAsDouble(HealthValue value, HealthDataType type) { + if (value is NumericHealthValue) { + // if(type == HealthDataType.BLOOD_OXYGEN) { + // return (value.numericValue.toDouble() * 100); + // } else { + return value.numericValue.toDouble(); + // } + } + return 0.0; + } +} diff --git a/lib/presentation/smartwatches/huawei_health_example.dart b/lib/presentation/smartwatches/huawei_health_example.dart new file mode 100644 index 0000000..4163d8b --- /dev/null +++ b/lib/presentation/smartwatches/huawei_health_example.dart @@ -0,0 +1,1563 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:huawei_health/huawei_health.dart'; + +const String packageName = 'com.ejada.hmg'; + +class HuaweiHealthExample extends StatefulWidget { + const HuaweiHealthExample({Key? key}) : super(key: key); + + @override + State createState() => _HuaweiHealthExampleState(); +} + +class _HuaweiHealthExampleState extends State { + /// Styles + static const TextStyle cardTitleTextStyle = TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ); + static const EdgeInsets componentPadding = EdgeInsets.all(8.0); + + /// Text Controllers for showing the logs of different modules + final TextEditingController _activityTextController = TextEditingController(); + final TextEditingController _dataTextController = TextEditingController(); + final TextEditingController _settingTextController = TextEditingController(); + final TextEditingController _autoRecorderTextController = TextEditingController(); + final TextEditingController _consentTextController = TextEditingController(); + final TextEditingController _healthTextController = TextEditingController(); + + /// Data controller reference to initialize at startup. + late DataController _dataController; + + String? accessToken = ''; + + @override + void initState() { + super.initState(); + if (!mounted) return; + // Initialize Event Callbacks + AutoRecorderController.autoRecorderStream.listen(_onAutoRecorderEvent); + // Initialize a DataController + initDataController(); + } + + /// Prints the specified text on both the console and the specified text controller. + void log( + String methodName, + TextEditingController controller, + LogOptions logOption, { + String? result = '', + String? error = '', + }) { + String log = ''; + switch (logOption) { + case LogOptions.call: + log = '$methodName called'; + break; + case LogOptions.success: + log = '$methodName [Success: $result] '; + break; + case LogOptions.error: + log = '$methodName [Error: $error] [Error Description: ${HiHealthStatusCodes.getStatusCodeMessage(error ?? '')}]'; + break; + case LogOptions.custom: + log = methodName; // Custom text + break; + } + debugPrint(log); + setState(() { + controller.text = '$log\n${controller.text}'; + }); + } + + /// Authorizes Huawei Health Kit for the user, with defined scopes. + void signIn() async { + // List of scopes to ask for authorization. + // + // Note: These scopes should also be authorized on the Huawei Developer Console. + final List scopes = [ + Scope.HEALTHKIT_ACTIVITY_READ, + Scope.HEALTHKIT_ACTIVITY_WRITE, + Scope.HEALTHKIT_BLOODGLUCOSE_READ, + Scope.HEALTHKIT_BLOODGLUCOSE_WRITE, + Scope.HEALTHKIT_CALORIES_READ, + Scope.HEALTHKIT_CALORIES_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_LOCATION_READ, + Scope.HEALTHKIT_LOCATION_WRITE, + Scope.HEALTHKIT_PULMONARY_READ, + Scope.HEALTHKIT_PULMONARY_WRITE, + Scope.HEALTHKIT_SLEEP_READ, + Scope.HEALTHKIT_SLEEP_WRITE, + Scope.HEALTHKIT_SPEED_READ, + Scope.HEALTHKIT_SPEED_WRITE, + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_STRENGTH_READ, + Scope.HEALTHKIT_STRENGTH_WRITE, + Scope.HEALTHKIT_BODYFAT_READ, + Scope.HEALTHKIT_BODYFAT_WRITE, + Scope.HEALTHKIT_NUTRITION_READ, + Scope.HEALTHKIT_NUTRITION_WRITE, + Scope.HEALTHKIT_BLOODPRESSURE_READ, + Scope.HEALTHKIT_BLOODPRESSURE_WRITE, + Scope.HEALTHKIT_BODYTEMPERATURE_READ, + Scope.HEALTHKIT_BODYTEMPERATURE_WRITE, + Scope.HEALTHKIT_OXYGENSTATURATION_READ, + Scope.HEALTHKIT_OXYGENSTATURATION_WRITE, + Scope.HEALTHKIT_REPRODUCTIVE_READ, + Scope.HEALTHKIT_REPRODUCTIVE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTRATE_REALTIME, + Scope.HEALTHKIT_STEP_REALTIME, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_STRESS_WRITE, + Scope.HEALTHKIT_STRESS_READ, + Scope.HEALTHKIT_OXYGEN_SATURATION_WRITE, + Scope.HEALTHKIT_OXYGEN_SATURATION_READ, + Scope.HEALTHKIT_HISTORYDATA_OPEN_WEEK, + Scope.HEALTHKIT_HISTORYDATA_OPEN_MONTH, + Scope.HEALTHKIT_HISTORYDATA_OPEN_YEAR, + ]; + try { + AuthHuaweiId? result = await HealthAuth.signIn(scopes); + debugPrint( + 'Granted Scopes for User(${result?.displayName}): ${result?.grantedScopes?.toString()}', + ); + showSnackBar( + 'Authorization Success.', + color: Colors.green, + ); + setState(() => accessToken = result?.accessToken); + } on PlatformException catch (e) { + debugPrint('Error on authorization, Error:${e.toString()}'); + showSnackBar( + 'Error on authorization, Error:${e.toString()}, Error Description: ' + '${HiHealthStatusCodes.getStatusCodeMessage(e.message ?? '')}', + ); + } + } + + // ActivityRecordsController + // + /// Adds an ActivityRecord with an ActivitySummary, time range is 2 hours from now. + Future addActivityRecord() async { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.call, + ); + DateTime startTime = DateTime.now().subtract(const Duration(hours: 2)); + DateTime endTime = DateTime.now(); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + startTime: startTime, + endTime: endTime, + id: 'ActivityRecordId0', + name: 'AddActivityRecord', + activityTypeId: HiHealthActivities.running, + description: 'This is a test for ActivityRecord', + activitySummary: ActivitySummary( + paceSummary: PaceSummary( + avgPace: 247.27626, + bestPace: 212.0, + britishPaceMap: { + '102802480': 365.0, + }, + britishPartTimeMap: { + '1.0': 263.0, + }, + partTimeMap: { + '1.0': 456.0, + }, + paceMap: { + '1.0': 263.0, + }, + ), + dataSummary: [ + SamplePoint( + dataType: DataType.DT_CONTINUOUS_DISTANCE_TOTAL, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_DISTANCE, 400), + timeUnit: TimeUnit.MILLISECONDS, + ), + SamplePoint( + dataType: DataType.POLYMERIZE_CONTINUOUS_SPEED_STATISTICS, + fieldValueOptions: FieldFloat(Field.FIELD_AVG, 60.0), + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + timeUnit: TimeUnit.MILLISECONDS, + ) + ..setFieldValue(Field.FIELD_MIN, 40.0) + ..setFieldValue(Field.FIELD_MAX, 80.0), + ]), + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + name: 'AddActivityRecord1923', + ); + + // You can use sampleSets to add more sample points to the sampling dataset. + // Build a list of sampling point objects and add it to the sampling dataSet + List samplePoints = [ + SamplePoint( + dataCollector: dataCollector, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_STEP_RATE, 10.0), + timeUnit: TimeUnit.MILLISECONDS, + ), + ]; + SampleSet sampleSet = SampleSet( + dataCollector, + samplePoints, + ); + + try { + await ActivityRecordsController.addActivityRecord( + ActivityRecordInsertOptions( + activityRecord: activityRecord, + sampleSets: [ + sampleSet, + ], + ), + ); + log( + 'addActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Obtains saved ActivityRecords between yesterday and now, + /// with the DT_CONTINUOUS_STEPS_DELTA data type + void getActivityRecord() async { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Create start time that will be used to read activity record. + DateTime startTime = DateTime.now().subtract(const Duration(days: 1)); + + // Create end time that will be used to read activity record. + DateTime endTime = DateTime.now().add(const Duration(hours: 3)); + + ActivityRecordReadOptions activityRecordReadOptions = ActivityRecordReadOptions( + activityRecordId: "ActivityRecordId0", + activityRecordName: null, + startTime: startTime, + endTime: endTime, + timeUnit: TimeUnit.MILLISECONDS, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + ); + try { + List result = await ActivityRecordsController.getActivityRecord( + activityRecordReadOptions, + ); + log( + 'getActivityRecord', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Starts the ActivityRecord with the id:`ActivityRecordRun1` + void beginActivityRecord() async { + try { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + id: 'ActivityRecordRun0', + name: 'BeginActivityRecord', + description: 'This is ActivityRecord begin test!', + activityTypeId: HiHealthActivities.running, + startTime: DateTime.now().subtract(const Duration(hours: 1)), + ); + await ActivityRecordsController.beginActivityRecord( + activityRecord, + ); + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Stops the ActivityRecord with the id:`ActivityRecordRun1` + void endActivityRecord() async { + try { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.call, + ); + final List result = await ActivityRecordsController.endActivityRecord( + 'ActivityRecordRun0', + ); + // Return the list of activity records that have stopped + log( + 'endActivityRecord', + _activityTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Ends all the ongoing activity records. + /// + /// Result list will be null if there is no ongoing activity record. + void endAllActivityRecords() async { + try { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.call, + ); + // Return the list of activity records that have stopped + List result = await ActivityRecordsController.endAllActivityRecords(); + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + // + // + // End of ActivityRecordsController Methods + + // DataController Methods + // + // + /// Initializes a DataController instance with a list of HiHealtOptions. + void initDataController() async { + if (!mounted) return; + log( + 'init', + _dataTextController, + LogOptions.call, + ); + try { + _dataController = await DataController.init(); + log( + 'init', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'init', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Clears all the data inserted by the app. + void clearAll() async { + log('clearAll', _dataTextController, LogOptions.call); + try { + await _dataController.clearAll(); + log('clearAll', _dataTextController, LogOptions.success); + } on PlatformException catch (e) { + log('clearAll', _dataTextController, LogOptions.error, error: e.message); + } + } + + /// Deletes DT_CONTINUOUS_STEPS_DELTA type data by the specified time range. + void delete() async { + log( + 'delete', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the deletion: start time and end time. + DeleteOptions deleteOptions = DeleteOptions( + dataCollectors: [dataCollector], + startTime: DateTime.parse('2020-10-10 08:00:00'), + endTime: DateTime.parse('2020-10-10 12:30:00'), + ); + + // Call the api with the constructed DeleteOptions instance. + try { + _dataController.delete(deleteOptions); + log( + 'delete', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'delete', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Inserts a sampling set with the DT_CONTINUOUS_STEPS_DELTA data type at the + /// specified start and end dates. + void insert() async { + log( + 'insert', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 100, + ), + ), + ], + ); + // Call the api with the constructed sample set. + try { + _dataController.insert(sampleSet); + log( + 'insert', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'insert', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // Reads the user data between the specified start and end dates. + void read() async { + log( + 'read', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the query: start time and end time. + ReadOptions readOptions = ReadOptions( + dataCollectors: [ + dataCollector, + ], + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + )..groupByTime(10000); + + // Call the api with the constructed ReadOptions instance. + try { + ReadReply? readReply = await _dataController.read(readOptions); + log( + 'read', + _dataTextController, + LogOptions.success, + result: readReply.toString(), + ); + } on PlatformException catch (e) { + log( + 'read', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the daily summation between the dates: `2020.10.02` to `2020.12.15` for multiple data types. + /// Note that the time format is different for this method. + void readDailySummationList() async { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.call, + ); + try { + List? sampleSets = await _dataController.readDailySummationList( + [DataType.DT_CONTINUOUS_STEPS_DELTA, DataType.DT_CONTINUOUS_CALORIES_BURNT], + 20201002, + 20201003, + ); + log( + 'readDailySummationList', + _dataTextController, + LogOptions.success, + result: sampleSets.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the steps summation for today. + void readTodaySummation() async { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.call, + ); + try { + SampleSet? sampleSet = await _dataController.readTodaySummation( + DataType.DT_CONTINUOUS_STEPS_DELTA, + ); + log( + 'readTodaySummation', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Updates DT_CONTINUOUS_STEPS_DELTA for the specified dates. + void update() async { + log( + 'update', + _dataTextController, + LogOptions.call, + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-12-12 09:00:00'), + endTime: DateTime.parse('2020-12-12 09:05:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 120, + ), + ), + ], + ); + + // Build a parameter object for the update. + // Note: (1) The start time of the modified object updateOptions can not be greater than the minimum + // value of the start time of all sample data points in the modified data sample set + // (2) The end time of the modified object updateOptions can not be less than the maximum value of the + // end time of all sample data points in the modified data sample set + UpdateOptions updateOptions = UpdateOptions( + startTime: DateTime.parse('2020-12-12 08:00:00'), + endTime: DateTime.parse('2020-12-12 09:25:00'), + sampleSet: sampleSet, + ); + try { + await _dataController.update(updateOptions); + log( + 'update', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'update', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of DataController Methods + + // SettingController Methods + // + /// Adds a custom DataType with the FIELD_ALTITUDE. + void addDataType() async { + log( + 'addDataType', + _settingTextController, + LogOptions.call, + ); + try { + // The name of the created data type must be prefixed with the package name + // of the app. Otherwise, the creation fails. If the same data type is tried to + // be added again an exception will be thrown. + DataTypeAddOptions options = DataTypeAddOptions( + '$packageName.myCustomDataType', + [ + const Field.newIntField('myIntField'), + Field.FIELD_ALTITUDE, + ], + ); + final DataType dataTypeResult = await SettingController.addDataType( + options, + ); + log( + 'addDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'addDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the inserted data type on the [addDataType] method. + void readDataType() async { + log( + 'readDataType', + _settingTextController, + LogOptions.call, + ); + try { + final DataType dataTypeResult = await SettingController.readDataType( + '$packageName.myCustomDataType', + ); + log( + 'readDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Disables the Health Kit function, cancels user authorization, and cancels + /// all data records. (The task takes effect in 24 hours.) + void disableHiHealth() async { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.disableHiHealth(); + log( + 'disableHiHealth', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. Redirects the user to + /// the Authorization screen if the permissions are not given. + void checkHealthAppAuthorization() async { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.checkHealthAppAuthorization(); + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. If authorized `true` + /// value would be returned. + void getHealthAppAuthorization() async { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + final bool result = await SettingController.getHealthAppAuthorization(); + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void requestAuth() async { + final HealthKitAuthResult res = await SettingController.requestAuthorizationIntent( + [ + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + ], + true, + ); + debugPrint(res.authAccount?.accessToken); + } + + // + // + // End of SettingController Methods + + // AutoRecorderController Methods + // + // + // Callback function for AutoRecorderStream event. + void _onAutoRecorderEvent(SamplePoint? res) { + log( + '[AutoRecorderEvent] obtained, SamplePoint Field Value is ${res?.fieldValues?.toString()}', + _autoRecorderTextController, + LogOptions.custom, + ); + } + + /// Starts an Android Foreground Service to count the steps of the user. + /// The steps will be emitted to the AutoRecorderStream. + void startRecord() async { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.startRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + NotificationProperties( + title: 'HMS Flutter Health Demo', + text: 'Counting steps', + subText: 'this is a subtext', + ticker: 'this is a ticker', + showChronometer: true, + ), + ); + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Ends the Foreground service and stops the step count events. + void stopRecord() async { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.stopRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + ); + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of AutoRecorderController Methods + + // ConsentController Methods + // + /// Obtains the application id from the agconnect-services.json file. + void getAppId() async { + log( + 'getAppId', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + log( + 'getAppId', + _consentTextController, + LogOptions.success, + result: appId, + ); + } on PlatformException catch (e) { + log( + 'getAppId', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Gets the granted permission scopes for the app. + void getScopes() async { + log( + 'getScopes', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + final ScopeLangItem scopeLangItem = await ConsentsController.getScopes( + 'en-gb', + appId, + ); + log( + 'getScopes', + _consentTextController, + LogOptions.success, + result: scopeLangItem.toString(), + ); + } on PlatformException catch (e) { + log( + 'getScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes all the permissions that authorized for this app. + void revoke() async { + log( + 'revoke', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + await ConsentsController.revoke(appId); + log( + 'revoke', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revoke', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes the distance read/write permissions for the app. + void revokeWithScopes() async { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.call, + ); + try { + // Obtain the application id. + final String appId = await ConsentsController.getAppId(); + // Call the revokeWithScopes method with desired scopes. + await ConsentsController.revokeWithScopes( + appId, + [ + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + ], + ); + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of ConsentController Methods + + // HealthController Methods + // + void addHealthRecord() async { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + DataCollector contDataCollector = DataCollector( + dataStreamName: 'contDataCollector', + packageName: packageName, + dataType: DataType.POLYMERIZE_CONTINUOUS_HEART_RATE_STATISTICS, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + DataCollector instDataCollector = DataCollector( + dataStreamName: 'instDataCollector', + packageName: packageName, + dataType: DataType.DT_INSTANTANEOUS_HEART_RATE, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + List subDataDetails = [ + SampleSet(instDataCollector, [ + SamplePoint( + dataCollector: instDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_BPM, 88.0) + ]) + ]; + + List subDataSummary = [ + SamplePoint( + dataCollector: contDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_AVG, 90.0) + ..setFieldValue(Field.FIELD_MAX, 100.0) + ..setFieldValue(Field.FIELD_MIN, 80.0) + ..setFieldValue(Field.LAST, 85.0) + ]; + + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setSubDataSummary(subDataSummary) + ..setSubDataDetails(subDataDetails) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 42.0) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 48.0) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 42.0) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.0); + + final String? result = await HealthRecordController.addHealthRecord( + HealthRecordInsertOptions( + healthRecord: healthRecord, + ), + ); + log( + 'addHealthRecord', + _healthTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void getHealthRecord() async { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + HealthRecordReply result = await HealthRecordController.getHealthRecord( + HealthRecordReadOptions( + packageName: packageName, + ) + ..setSubDataTypeList( + [ + DataType.DT_INSTANTANEOUS_HEART_RATE, + ], + ) + ..setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS, + ) + ..readByDataType( + HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + ) + ..readHealthRecordsFromAllApps(), + ); + log( + 'getHealthRecord', + _healthTextController, + LogOptions.success, + result: result.healthRecords[0].toJson(), + ); + } on PlatformException catch (e) { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void updateHealthRecord() async { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2022, 10, 11); + final DateTime endTime = DateTime(2022, 10, 12); + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 41.9) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 49.1) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 41.1) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.1); + await HealthRecordController.updateHealthRecord( + HealthRecordUpdateOptions( + healthRecord: healthRecord, + healthRecordId: '', + ), + ); + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void deleteHealthRecord() async { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + await HealthRecordController.deleteHealthRecord( + HealthRecordDeleteOptions( + startTime: DateTime.now().subtract(const Duration(days: 14)), + endTime: DateTime.now(), + )..setHealthRecordIds( + [ + '', + ], + ), + ); + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of HealthController Methods + + // App's widgets. + // + // + Widget expansionCard({ + required String titleText, + required List children, + }) { + return Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: ExpansionTile( + title: Text( + titleText, + style: cardTitleTextStyle, + ), + children: children, + ), + ); + } + + Widget loggingArea( + TextEditingController moduleTextController, + ) { + return Column( + children: [ + Container( + margin: componentPadding, + padding: const EdgeInsets.all(8.0), + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + border: Border.all(color: Colors.black12), + ), + child: TextField( + readOnly: true, + maxLines: 15, + controller: moduleTextController, + decoration: const InputDecoration( + enabledBorder: InputBorder.none, + ), + ), + ), + TextButton( + child: const Text('Clear Log'), + onPressed: () => setState(() { + moduleTextController.text = ''; + }), + ) + ], + ); + } + + void showSnackBar( + String text, { + Color color = Colors.blue, + }) { + final SnackBar snackBar = SnackBar( + content: Text(text), + backgroundColor: color, + action: SnackBarAction( + label: 'Close', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + }, + ), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text( + 'Huawei Health Kit', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + elevation: 0.0, + actions: [ + IconButton( + onPressed: requestAuth, + icon: const Icon(Icons.ac_unit), + ), + ], + ), + body: Builder( + builder: (BuildContext context) { + return ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + children: [ + // Sign In Widgets + Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: componentPadding, + child: Text( + 'Tap to SignIn button to obtain the HMS Account to complete ' + 'login and authorization, and then use other buttons ' + 'to try the related API functions.', + textAlign: TextAlign.center, + ), + ), + const Padding( + padding: componentPadding, + child: Text( + 'Note: If the login page is not displayed, change the package ' + 'name, AppID, and configure the signature file by referring ' + 'to the developer guide on the official website.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue, + ), + ), + ), + Container( + padding: componentPadding, + width: double.infinity, + child: OutlinedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Colors.blue, + ), + ), + child: const Text( + 'SignIn', + style: TextStyle( + color: Colors.white, + ), + ), + onPressed: () => signIn(), + ), + ), + ], + ), + ), + + // ActivityRecordsController + expansionCard( + titleText: 'ActivityRecords Controller', + children: [ + loggingArea(_activityTextController), + ListTile( + title: const Text('AddActivityRecord'), + onTap: () => addActivityRecord(), + ), + ListTile( + title: const Text('GetActivityRecord'), + onTap: () => getActivityRecord(), + ), + ListTile( + title: const Text('beginActivityRecord'), + onTap: () => beginActivityRecord(), + ), + ListTile( + title: const Text('endActivityRecord'), + onTap: () => endActivityRecord(), + ), + ListTile( + title: const Text('endAllActivityRecords'), + onTap: () => endAllActivityRecords(), + ), + ], + ), + // DataController Widgets + expansionCard( + titleText: 'DataController', + children: [ + loggingArea(_dataTextController), + ListTile( + title: const Text('readTodaySummation'), + onTap: () => readTodaySummation(), + ), + ListTile( + title: const Text('readDailySummationList'), + onTap: () => readDailySummationList(), + ), + ListTile( + title: const Text('insert'), + onTap: () => insert(), + ), + ListTile( + title: const Text('read'), + onTap: () => read(), + ), + ListTile( + title: const Text('update'), + onTap: () => update(), + ), + ListTile( + title: const Text('delete'), + onTap: () => delete(), + ), + ListTile( + title: const Text('clearAll'), + onTap: () => clearAll(), + ), + ], + ), + // SettingController Widgets. + expansionCard( + titleText: 'SettingController', + children: [ + loggingArea(_settingTextController), + ListTile( + title: const Text('addDataType'), + onTap: () => addDataType(), + ), + ListTile( + title: const Text('readDataType'), + onTap: () => readDataType(), + ), + ListTile( + title: const Text('disableHiHealth'), + onTap: () => disableHiHealth(), + ), + ListTile( + title: const Text('checkHealthAppAuthorization'), + onTap: () => checkHealthAppAuthorization(), + ), + ListTile( + title: const Text('getHealthAppAuthorization'), + onTap: () => getHealthAppAuthorization(), + ), + ], + ), + // AutoRecorderController Widgets + expansionCard( + titleText: 'AutoRecorderController', + children: [ + loggingArea(_autoRecorderTextController), + ListTile( + title: const Text('startRecord'), + onTap: () => startRecord(), + ), + ListTile( + title: const Text('stopRecord'), + onTap: () => stopRecord(), + ), + ], + ), + // Consent Controller Widgets + expansionCard( + titleText: 'ConsentController', + children: [ + loggingArea(_consentTextController), + ListTile( + title: const Text('getAppId'), + onTap: () => getAppId(), + ), + ListTile( + title: const Text('getScopes'), + onTap: () => getScopes(), + ), + ListTile( + title: const Text('revoke'), + onTap: () => revoke(), + ), + ListTile( + title: const Text('revokeWithScopes'), + onTap: () => revokeWithScopes(), + ), + ], + ), + + // Health Controller Widgets + expansionCard( + titleText: 'HealthController', + children: [ + loggingArea(_healthTextController), + ListTile( + title: const Text('addHealthRecord'), + onTap: () => addHealthRecord(), + ), + ListTile( + title: const Text('getHealthRecord'), + onTap: () => getHealthRecord(), + ), + ListTile( + title: const Text('updateHealthRecord'), + onTap: () => updateHealthRecord(), + ), + ListTile( + title: const Text('deleteHealthRecord'), + onTap: () => deleteHealthRecord(), + ), + ], + ), + ], + ); + }, + ), + ); + } +} + +/// Options for logging. +enum LogOptions { + call, + success, + error, + custom, +} diff --git a/lib/presentation/smartwatches/smartwatch_instructions_page.dart b/lib/presentation/smartwatches/smartwatch_instructions_page.dart new file mode 100644 index 0000000..7f17f5a --- /dev/null +++ b/lib/presentation/smartwatches/smartwatch_instructions_page.dart @@ -0,0 +1,466 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/health_dashboard/health_dashboard.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SmartwatchInstructionsPage extends StatelessWidget { + const SmartwatchInstructionsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.smartWatches.tr(), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: SingleChildScrollView( + child: Platform.isIOS ? getIOSInstructionsUI(context) : getAndroidInstructionsUI(context), + ), + ), + ), + ); + } + + Widget getAndroidInstructionsUI(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Supported Smart Watches".needTranslation.toText20(isBold: true), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_ultra.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch Ultra", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 8 Classic", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_8.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 8", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_7_classic.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 7 Classic", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_7.webp", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 7", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_fit_3.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Fit3", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + SizedBox(height: 12), + "Please make sure that your Samsung Watch is connected to your Phone, is actively synced & updated.".needTranslation.toText14(isBold: true), + SizedBox(height: 12), + "Before syncing data, please make sure that you have followed the instructions properly.".needTranslation.toText14(isBold: true), + SizedBox(height: 12), + InkWell( + onTap: () { + showInstructionsDialog(context); + }, + child: "View watch instructions".needTranslation.toText12(isBold: true, color: AppColors.textColor, isUnderLine: true)), + SizedBox( + height: 130.h, + ), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + await Health().getHealthConnectSdkStatus().then((val) { + print('Health Connect SDK Status: $val'); + if (val == HealthConnectSdkStatus.sdkAvailable) { + Navigator.of(context).push( + CustomPageRoute( + page: HealthDashboard(), + ), + ); + } else { + getIt.get().showErrorBottomSheet( + message: "Seems like you do not have Health Connect App installed. Please install it from the Play Store to sync your health data.".needTranslation, + onOkPressed: () { + Navigator.pop(context); + Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"); + launchUrl(uri, mode: LaunchMode.externalApplication); + }); + return; + } + }); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + // DefaultButton( + // LocaleKeys.confirm.tr(context: context), + // () async { + // await Health().getHealthConnectSdkStatus().then((val) { + // print('Health Connect SDK Status: $val'); + // if (val == HealthConnectSdkStatus.sdkAvailable) { + // Navigator.of(context).push( + // FadePage( + // page: HealthDashboard(), // Replace with the actual vital signs page + // // page: HealthApp(), // Replace with the actual vital signs page + // ), + // ); + // } else { + // Utils.showAppDialog(context, LocaleKeys.error.tr(context: context), LocaleKeys.healthConnectNotInstalled.tr(), () { + // Navigator.pop(context); + // Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"); + // launchUrl(uri, mode: LaunchMode.externalApplication); + // }); + // return; + // } + // }); + // }, + // ) + ], + ); + } + + Widget getIOSInstructionsUI(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Supported Smart Watches".needTranslation.toText20(isBold: true), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-1.jpeg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 5", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-2.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 6", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-3.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 7", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-4.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 8", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-5.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 9", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/Apple-Watch-6.png", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 10", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + SizedBox(height: 12), + "Please make sure that your Apple Watch is connected to your iPhone, is actively synced & updated.".toText14(isBold: true), + SizedBox(height: 12), + "Before syncing data, please make sure that you have followed the instructions properly.".toText14(isBold: true), + SizedBox(height: 12), + InkWell( + onTap: () { + showInstructionsDialog(context); + }, + child: "View watch instructions".toText12(isBold: true, color: AppColors.textColor, isUnderLine: true), + ), + SizedBox( + height: 130.h, + ), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).push( + CustomPageRoute( + page: HealthDashboard(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ], + ); + } + + showInstructionsDialog(BuildContext context) { + showGeneralDialog( + barrierColor: Colors.black.withOpacity(0.5), + transitionBuilder: (context, a1, a2, widget) { + final curvedValue = Curves.easeInOutBack.transform(a1.value) - 1.0; + return Transform( + transform: Matrix4.translationValues(0.0, curvedValue * 200, 0.0), + child: Opacity( + opacity: a1.value, + child: Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 350.0, + padding: EdgeInsets.all(21), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "SmartWatch Tracker".toText18(), + IconButton( + icon: Icon( + Icons.close, + color: Color(0xff2E303A), + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (Platform.isIOS + ? "Make sure that you have installed 'Health' App & 'Watch' App from Apple Store." + : "Please make sure that your Samsung Watch is connected to your Phone, is actively synced & updated.") + .needTranslation + .toText14(), + SizedBox(height: 12), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ); + }, + transitionDuration: Duration(milliseconds: 500), + barrierDismissible: true, + barrierLabel: '', + context: context, + pageBuilder: (context, animation1, animation2) { + return SizedBox(); + //Chanbged By Aamir + }); + } +} diff --git a/lib/presentation/smartwatches/widgets/health_chart.dart b/lib/presentation/smartwatches/widgets/health_chart.dart new file mode 100644 index 0000000..fe6ae78 --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_chart.dart @@ -0,0 +1,87 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class HealthChart extends StatelessWidget { + final List spots; + final String title; + final Color gradientColor; + + const HealthChart({ + Key? key, + required this.spots, + required this.title, + required this.gradientColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 200, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Expanded( + child: LineChart( + LineChartData( + gridData: FlGridData(show: false), + titlesData: FlTitlesData(show: false), + borderData: FlBorderData(show: false), + minX: 0, + maxX: spots.length.toDouble() - 1, + minY: spots.map((e) => e.y).reduce((a, b) => a < b ? a : b), + maxY: spots.map((e) => e.y).reduce((a, b) => a > b ? a : b), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + gradient: LinearGradient( + colors: [ + gradientColor.withOpacity(0.5), + gradientColor, + ], + ), + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + gradientColor.withOpacity(0.1), + gradientColor.withOpacity(0.2), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/smartwatches/widgets/health_metric.dart b/lib/presentation/smartwatches/widgets/health_metric.dart new file mode 100644 index 0000000..5375966 --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_metric.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthMetricInfo { + final HealthDataType type; + final String nameEn; + final String nameAr; + final String unit; + final Color color; + final IconData icon; + final String description; + final double minHealthyValue; + final double maxHealthyValue; + final String svgIcon; + + const HealthMetricInfo( + {required this.type, + required this.nameEn, + required this.nameAr, + required this.unit, + required this.color, + required this.icon, + required this.description, + required this.minHealthyValue, + required this.maxHealthyValue, + required this.svgIcon}); +} + +class HealthMetrics { + static final metrics = [ + HealthMetricInfo( + type: HealthDataType.HEART_RATE, + nameEn: "Heart Rate", + nameAr: "معدل النبض", + unit: 'BPM', + color: AppColors.primaryRedColor, + icon: Icons.favorite, + description: "Your heart rate indicates how many times your heart beats per minute".needTranslation, + minHealthyValue: 60, + maxHealthyValue: 100, + svgIcon: "assets/images/smartwatches/heartrate_icon.svg"), + HealthMetricInfo( + type: HealthDataType.BLOOD_OXYGEN, + nameEn: "Blood Oxygen", + nameAr: "أكسجين الدم", + unit: '%', + // color: Colors.blue, + color: Color(0xff3A3558), + icon: Icons.air, + description: "Blood oxygen level indicates how much oxygen your red blood cells are carrying".needTranslation, + minHealthyValue: 95, + maxHealthyValue: 100, + svgIcon: "assets/images/smartwatches/bloodoxygen_icon.svg"), + HealthMetricInfo( + type: HealthDataType.STEPS, + nameEn: "Steps", + nameAr: "خطوات", + unit: 'steps', + // color: Colors.green, + color: Color(0xff3263B8), + icon: Icons.directions_walk, + description: "Number of steps taken throughout the day".needTranslation, + minHealthyValue: 7000, + maxHealthyValue: 15000, + svgIcon: "assets/images/smartwatches/steps_icon.svg"), + HealthMetricInfo( + type: Platform.isIOS ? HealthDataType.ACTIVE_ENERGY_BURNED : HealthDataType.TOTAL_CALORIES_BURNED, + nameEn: "Active Calories", + nameAr: "السعرات الحرارية النشطة", + unit: 'kcal', + color: Color(0xffD59E95), + icon: Icons.local_fire_department, + description: "Calories burned during physical activity".needTranslation, + minHealthyValue: 300, + maxHealthyValue: 1000, + svgIcon: "assets/images/smartwatches/calories_icon.svg"), + HealthMetricInfo( + type: Platform.isIOS ? HealthDataType.DISTANCE_WALKING_RUNNING : HealthDataType.DISTANCE_DELTA, + nameEn: "Distance Covered", + nameAr: "المسافة المغطاة", + unit: 'KMs', + // color: mainPurple, + color: Color(0xff6A46F5), + icon: Icons.directions_run, + description: "Distance covered throughout the day".needTranslation, + minHealthyValue: 3, + maxHealthyValue: 10, + svgIcon: "assets/images/smartwatches/distance_icon.svg"), + // Add more metrics as needed + ]; +} diff --git a/lib/presentation/smartwatches/widgets/health_metric_card.dart b/lib/presentation/smartwatches/widgets/health_metric_card.dart new file mode 100644 index 0000000..023f41b --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_metric_card.dart @@ -0,0 +1,151 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:health/health.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthMetricCard extends StatelessWidget { + final HealthMetricInfo metric; + final List data; + final VoidCallback onTap; + + const HealthMetricCard({ + Key? key, + required this.metric, + required this.data, + required this.onTap, + }) : super(key: key); + + // Helper method to convert HealthValue to double + double _getValueAsDouble(HealthValue value, HealthDataType type) { + if (value is NumericHealthValue) { + return value.numericValue.toDouble(); + } + return 0.0; + } + + @override + Widget build(BuildContext context) { + final latestValue = data.isNotEmpty ? _getValueAsDouble(data.last.value, data.last.type) : 0.0; + final isHealthy = latestValue >= metric.minHealthyValue && latestValue <= metric.maxHealthyValue; + + return Card( + color: AppColors.whiteColor, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + metric.svgIcon, + height: 30.h, + width: 30.h, + fit: BoxFit.contain, + // color: color, + ), + // Icon( + // metric.icon, + // color: metric.color, + // size: 30, + // ), + if (data.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isHealthy ? Colors.green[100] : Color(0xffD7D2F3), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + isHealthy ? "Healthy".needTranslation : 'Warning'.needTranslation, + style: TextStyle( + color: isHealthy ? Colors.green[700] : AppColors.blackColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText18(isBold: true, maxlines: 1), + // Text( + // metric.name, + // style: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.bold, + // ), + // ), + const SizedBox(height: 8), + Text( + data.isEmpty ? LocaleKeys.noDataAvailable.tr(context: context) : '${latestValue.toStringAsFixed(metric.type == HealthDataType.STEPS ? 0 : 1)} ${metric.unit}', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: metric.color, + ), + ), + if (data.isNotEmpty) ...[ + const SizedBox(height: 16), + SizedBox( + height: 100, + child: LineChart( + LineChartData( + gridData: FlGridData(show: false), + titlesData: FlTitlesData(show: false), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: _convertToSpots(data), + isCurved: true, + color: metric.color, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: metric.color.withOpacity(0.1), + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ), + ); + } + + List _convertToSpots(List data) { + if (data.isEmpty) return []; + + return List.generate( + data.length, + (index) => FlSpot( + index.toDouble(), + _getValueAsDouble(data[index].value, data[index].type), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart new file mode 100644 index 0000000..c23b956 --- /dev/null +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class OrganSelectorPage extends StatefulWidget { + const OrganSelectorPage({super.key}); + + @override + State createState() => _OrganSelectorPageState(); +} + +class _OrganSelectorPageState extends State { + late final AppState _appState; + + @override + void initState() { + super.initState(); + _appState = getIt.get(); + } + + void _onNextPressed(SymptomsCheckerViewModel viewModel) { + if (!viewModel.validateSelection()) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select at least one organ'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + return; + } + + context.navigateWithName(AppRoutes.symptomsSelectorScreen); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: _buildAppBar(), + body: Consumer( + builder: (context, viewModel, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(), + SizedBox(height: 8.h), + Expanded( + child: Stack( + children: [ + _buildBodyViewer(viewModel), + _buildViewToggleButtons(viewModel), + _buildBottomSheet(viewModel), + ], + ), + ), + ], + ); + }, + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: AppColors.bgScaffoldColor, + elevation: 0, + leading: Transform.flip( + flipX: _appState.isArabic(), + child: IconButton( + icon: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + width: 32.h, + height: 32.h, + ), + padding: const EdgeInsets.only(left: 12), + onPressed: () => Navigator.pop(context), + highlightColor: Colors.transparent, + ), + ), + ); + } + + Widget _buildTitle() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + "Organ Selector".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 22.f, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { + return Padding( + padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: InteractiveBodyWidget( + key: ValueKey(viewModel.currentView), + bodyImageAsset: viewModel.currentView == BodyView.front ? AppAssets.fullBodyFront : AppAssets.fullBodyBack, + organs: viewModel.currentOrgans, + selectedOrganIds: viewModel.selectedOrganIds, + onOrganTap: viewModel.toggleOrganSelection, + isBodyHidden: viewModel.isBodyHidden, + tooltipOrganId: viewModel.tooltipOrganId, + ), + ), + ); + } + + Widget _build3DFlipTransition(Widget child, Animation animation) { + final rotateAnimation = Tween(begin: 0.5, end: 0.0).animate( + CurvedAnimation(parent: animation, curve: Curves.easeInOut), + ); + + return AnimatedBuilder( + animation: rotateAnimation, + builder: (context, child) { + final angle = rotateAnimation.value * 3.14159; + final transform = Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(angle); + + return Transform( + transform: transform, + alignment: Alignment.center, + child: child, + ); + }, + child: child, + ); + } + + Widget _buildViewToggleButtons(SymptomsCheckerViewModel viewModel) { + return Positioned( + left: 16.w, + top: 36.h, + child: Column( + children: [ + _buildToggleButton( + icon: AppAssets.bodyIcon, + onTap: viewModel.toggleIsBodyHidden, + ), + SizedBox(height: 16.h), + _buildToggleButton( + icon: AppAssets.rotateIcon, + onTap: viewModel.toggleView, + ), + ], + ), + ); + } + + Widget _buildToggleButton({required String icon, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 48.w, + height: 48.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 11.r, + ), + child: Center( + child: Utils.buildSvgWithAssets( + icon: icon, + width: 25.w, + height: 25.h, + ), + ), + ), + ); + } + + Widget _buildBottomSheet(SymptomsCheckerViewModel viewModel) { + return Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: SafeArea( + top: false, + child: Stack( + clipBehavior: Clip.none, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (viewModel.isBottomSheetExpanded) ...[ + SizedBox(height: 20.h), + _buildBottomSheetTitle(), + SizedBox(height: 12.h), + _buildSelectedOrgansContent(viewModel), + SizedBox(height: 16.h), + _buildNextButton(viewModel), + SizedBox(height: 16.h), + ] else + SizedBox(height: 20.h), + ], + ), + _buildExpandCollapseButton(viewModel), + ], + ), + ), + ), + ); + } + + Widget _buildBottomSheetTitle() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + 'Selected Organs'.needTranslation, + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ); + } + + Widget _buildSelectedOrgansContent(SymptomsCheckerViewModel viewModel) { + if (viewModel.selectedOrgans.isEmpty) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + 'No organs selected yet'.needTranslation, + style: TextStyle( + color: AppColors.greyTextColor, + fontSize: 14.f, + ), + ), + ); + } + + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: 150.h), + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Wrap( + spacing: 8.w, + runSpacing: 8.h, + children: viewModel.selectedOrgans.map((organ) { + return AppCustomChipWidget( + labelText: organ.description, + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + deleteIcon: AppAssets.cancel, + deleteIconColor: AppColors.primaryRedColor, + deleteIconHasColor: false, + onDeleteTap: () { + viewModel.removeOrgan(organ.id); + }); + }).toList(), + ), + ), + ); + } + + Widget _buildNextButton(SymptomsCheckerViewModel viewModel) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: CustomButton( + text: 'Next'.needTranslation, + onPressed: () => _onNextPressed(viewModel), + isDisabled: viewModel.selectedOrgans.isEmpty, + backgroundColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + ), + ); + } + + Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) { + return Positioned( + right: 24.w, + top: -24.h, + child: GestureDetector( + onTap: viewModel.toggleBottomSheet, + behavior: HitTestBehavior.opaque, + child: Container( + width: 70.w, + height: 70.h, + alignment: Alignment.center, + child: Container( + width: 48.w, + height: 48.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 11.r, + ), + child: Center( + child: AnimatedRotation( + duration: const Duration(milliseconds: 300), + turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + width: 25.w, + height: 25.h, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart new file mode 100644 index 0000000..2c99515 --- /dev/null +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -0,0 +1,177 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.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/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class PossibleConditionsScreen extends StatelessWidget { + const PossibleConditionsScreen({super.key}); + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(16.w), + itemCount: 10, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget _buildPredictionsList(List conditions) { + if (conditions.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Text( + 'No Predictions available'.needTranslation, + style: TextStyle( + fontSize: 16.h, + color: AppColors.greyTextColor, + ), + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ListView.separated( + padding: EdgeInsets.zero, + itemCount: conditions.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemBuilder: (context, index) { + final conditionModel = conditions[index]; + return ConditionCard( + icon: conditionModel.icon, + title: conditionModel.title, + percentage: conditionModel.percentage, + tagText: conditionModel.tagText, + clinic: conditionModel.clinic, + symptoms: conditionModel.symptoms, + description: conditionModel.description, + possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum, + onActionPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('We are not available for a week. May you Rest In Peace :('), + backgroundColor: AppColors.primaryRedColor, + ), + ); + }, + ); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + ], + ); + } + + _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () => onConfirm(), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + _restartOrganSelection(BuildContext context) async { + final symptomsCheckerVm = context.read(); + symptomsCheckerVm.reset(); + context.pop(); + await Future.delayed(Duration(seconds: 1)).whenComplete(() => context.pop()); + } + + _navigateToLandingPage() { + NavigationService navigationService = getIt.get(); + navigationService.replaceAllRoutesAndNavigateToLanding(); + } + + _buildTrailingSection(BuildContext context) { + return Row( + children: [ + Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.refreshIcon, + height: 20.h, + width: 20.w, + iconColor: AppColors.textColor, + ), + ), + ).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _restartOrganSelection(context))), + Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.homeBorderedIcon, + height: 20.h, + width: 20.w, + iconColor: AppColors.textColor, + ), + ), + ).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _navigateToLandingPage())), + ], + ).paddingSymmetrical(20.w, 0); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Possible Conditions".needTranslation, + trailing: _buildTrailingSection(context), + child: Consumer( + builder: (context, symptomsCheckerViewModel, child) { + if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) { + return _buildLoadingShimmer(); + } + return _buildPredictionsList(dummyConditions); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart new file mode 100644 index 0000000..d4ff2cb --- /dev/null +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -0,0 +1,309 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.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'; +import 'package:provider/provider.dart'; + +class RiskFactorsScreen extends StatefulWidget { + const RiskFactorsScreen({super.key}); + + @override + State createState() => _RiskFactorsScreenState(); +} + +class _RiskFactorsScreenState extends State { + @override + void initState() { + super.initState(); + // Fetch risk factors based on selected symptoms + WidgetsBinding.instance.addPostFrameCallback((_) { + final viewModel = context.read(); + viewModel.fetchRiskFactors(); + }); + } + + void _onRiskFactorSelected(SymptomsCheckerViewModel viewModel, String riskFactorId) { + viewModel.toggleRiskFactorSelection(riskFactorId); + } + + void _onNextPressed(SymptomsCheckerViewModel viewModel) { + if (viewModel.hasSelectedRiskFactors) { + context.navigateWithName(AppRoutes.suggestionsScreen); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select at least one risk before proceeding'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + } + } + + void _onPreviousPressed() { + context.pop(); + } + + Widget _buildRiskFactorItem(SymptomsCheckerViewModel viewModel, String riskFactorId, String optionText) { + final bool selected = viewModel.isRiskFactorSelected(riskFactorId); + + return GestureDetector( + onTap: () => _onRiskFactorSelected(viewModel, riskFactorId), + child: Container( + margin: EdgeInsets.only(bottom: 16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: selected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(5.r), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), + ), + child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded( + child: optionText.toText14( + color: riskFactorId == "not_applicable" ? AppColors.errorColor : AppColors.textColor, + weight: FontWeight.w500, + )), + ], + ), + ), + ); + } + + Widget _buildRiskFactorsList(SymptomsCheckerViewModel viewModel) { + return Container( + key: ValueKey(viewModel.riskFactorsList.length), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...viewModel.riskFactorsList.map((factor) { + return _buildRiskFactorItem(viewModel, factor.id ?? '', factor.getDisplayName()); + }), + SizedBox(height: 12.w), + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + height: 1.3, + fontSize: 13.f, + fontWeight: FontWeight.w500, + color: AppColors.greyInfoTextColor, + ), + children: [ + TextSpan( + text: "Above you see the most common risk factors. Although /diagnosis may return questions about risk factors, " + .needTranslation, + ), + TextSpan( + text: "read more".needTranslation, + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // handle tap - navigate or show bottom sheet + debugPrint('Read more tapped'); + // Example: Navigator.push(context, MaterialPageRoute(builder: (_) => RiskFactorsDetailScreen())); + }, + ), + ], + ), + ), + ) + ], + ), + ], + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), + ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Risk Factor', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, viewModel, _) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Risk Factors".needTranslation, + leadingCallback: () => context.pop(), + child: viewModel.isRiskFactorsLoading + ? _buildLoadingShimmer() + : viewModel.riskFactorsList.isEmpty + ? _buildEmptyState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildRiskFactorsList(viewModel), + ], + ), + ), + ), + _buildStickyBottomCard(context, viewModel), + ], + ); + }, + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor), + SizedBox(height: 16.h), + Text( + 'No risk factors found'.needTranslation, + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 8.h), + Text( + 'Based on your selected symptoms, no additional risk factors were identified.'.needTranslation, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.f, + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPreviousPressed, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () => _onNextPressed(viewModel), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart new file mode 100644 index 0000000..f2aa71e --- /dev/null +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.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'; +import 'package:provider/provider.dart'; + +class SuggestionsScreen extends StatefulWidget { + const SuggestionsScreen({super.key}); + + @override + State createState() => _SuggestionsScreenState(); +} + +class _SuggestionsScreenState extends State { + @override + void initState() { + super.initState(); + // Initialize symptom groups based on selected organs + WidgetsBinding.instance.addPostFrameCallback((_) { + final viewModel = context.read(); + viewModel.fetchSuggestions(); + }); + } + + void _onOptionSelected(int optionIndex) {} + + void _onSuggestionSelected(SymptomsCheckerViewModel viewModel, String suggestionId) { + viewModel.toggleSuggestionsSelection(suggestionId); + } + + void _onNextPressed(SymptomsCheckerViewModel viewModel) { + if (viewModel.hasSelectedSuggestions) { + // Navigate to triage screen + context.navigateWithName(AppRoutes.triageScreen); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select at least one option before proceeding'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + } + } + + void _onPreviousPressed() { + context.pop(); + } + + Widget _buildSuggestionItem(SymptomsCheckerViewModel viewModel, String suggestionId, String optionText) { + final bool selected = viewModel.isSuggestionsSelected(suggestionId); + + return GestureDetector( + onTap: () => _onSuggestionSelected(viewModel, suggestionId), + child: Container( + margin: EdgeInsets.only(bottom: 16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: selected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(5.r), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), + ), + child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded( + child: optionText.toText14( + color: suggestionId == "not_applicable" ? AppColors.errorColor : AppColors.textColor, + weight: FontWeight.w500, + )), + ], + ), + ), + ); + } + + Widget _buildSuggestionsList(SymptomsCheckerViewModel viewModel) { + return Container( + key: ValueKey(viewModel.suggestionsList.length), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...viewModel.suggestionsList.map((factor) { + return _buildSuggestionItem(viewModel, factor.id ?? '', factor.getDisplayName()); + }), + SizedBox(height: 12.w), + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview".toText12( + color: AppColors.greyInfoTextColor, + ), + ) + ], + ), + ], + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), + ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Suggestion', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, viewModel, _) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Suggestions".needTranslation, + leadingCallback: () => context.pop(), + child: viewModel.isSuggestionsLoading + ? _buildLoadingShimmer() + : viewModel.suggestionsList.isEmpty + ? _buildEmptyState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildSuggestionsList(viewModel), + ], + ), + ), + ), + _buildStickyBottomCard(context, viewModel), + ], + ); + }, + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor), + SizedBox(height: 16.h), + Text( + 'No organs selected'.needTranslation, + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 8.h), + Text( + 'Please go back and select organs first'.needTranslation, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.f, + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPreviousPressed, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () => _onNextPressed(viewModel), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart new file mode 100644 index 0000000..522c5f8 --- /dev/null +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -0,0 +1,312 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/chip/custom_selectable_chip.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class SymptomsSelectorScreen extends StatefulWidget { + const SymptomsSelectorScreen({super.key}); + + @override + State createState() => _SymptomsSelectorScreenState(); +} + +class _SymptomsSelectorScreenState extends State { + @override + void initState() { + super.initState(); + // Initialize symptom groups based on selected organs + WidgetsBinding.instance.addPostFrameCallback((_) { + final viewModel = context.read(); + viewModel.initializeSymptomGroups(); + }); + } + + void _onNextPressed(SymptomsCheckerViewModel viewModel) { + if (viewModel.hasSelectedSymptoms) { + // Navigate to triage screen + context.navigateWithName(AppRoutes.riskFactorsScreen); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select at least one symptom before proceeding'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + } + } + + void _onPreviousPressed() { + context.pop(); + } + + _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () => onConfirm(), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, viewModel, _) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Symptoms Selector".needTranslation, + leadingCallback: () => _buildConfirmationBottomSheet( + context: context, + onConfirm: () => { + context.pop(), + context.pop(), + }), + child: viewModel.isBodySymptomsLoading + ? _buildLoadingShimmer() + : viewModel.organSymptomsResults.isEmpty + ? _buildEmptyState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ...viewModel.organSymptomsResults.map((organResult) { + // Find matching organ ID from selected organs + String? organId; + String? organName; + for (var organ in viewModel.selectedOrgans) { + if (organ.name == organResult.name) { + organId = organ.id; + organName = organ.description; + break; + } + } + + if (organId != null && organResult.bodySymptoms != null) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildSymptomsSelectionCard( + viewModel, + organId, + organName ?? organResult.name ?? '', + organResult.bodySymptoms!, + ), + ); + } + return SizedBox.shrink(); + }), + ], + ), + ), + ), + if (!viewModel.isBodySymptomsLoading) ...[ + _buildStickyBottomCard(context, viewModel), + ], + ], + ); + }, + ), + ); + } + + Widget _buildSymptomsSelectionCard( + SymptomsCheckerViewModel viewModel, + String organId, + String organName, + List symptoms, + ) { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + 'Possible symptoms related to "$organName"', + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + ], + ), + SizedBox(height: 24.h), + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: symptoms.map((symptom) { + final symptomId = symptom.id ?? ''; + final symptomLabel = symptom.getDisplayName(false); // Pass isArabic from AppState if needed + bool isSelected = viewModel.isSymptomSelected(organId, symptomId); + return GestureDetector( + onTap: () => viewModel.toggleSymptomSelection(organId, symptomId), + child: CustomSelectableChip( + label: symptomLabel, + selected: isSelected, + activeColor: AppColors.primaryRedBorderColor, + activeTextColor: AppColors.primaryRedBorderColor, + inactiveBorderColor: AppColors.bottomNAVBorder, + inactiveTextColor: AppColors.textColor, + ), + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), + ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Loading..', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor), + SizedBox(height: 16.h), + Text( + 'No organs selected'.needTranslation, + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 8.h), + Text( + 'Please go back and select organs first'.needTranslation, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.f, + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPreviousPressed, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () => _onNextPressed(viewModel), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart new file mode 100644 index 0000000..ff0482e --- /dev/null +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -0,0 +1,286 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; + +class TriageScreen extends StatefulWidget { + const TriageScreen({super.key}); + + @override + State createState() => _TriageScreenState(); +} + +class _TriageScreenState extends State { + late List triageQuestions; + int currentQuestionIndex = 0; + + @override + void initState() { + super.initState(); + triageQuestions = TriageQuestionsData.getSampleTriageQuestions(); + } + + TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex]; + + bool get isFirstQuestion => currentQuestionIndex == 0; + + bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1; + + void _onOptionSelected(int optionIndex) { + setState(() { + currentQuestion.selectOption(optionIndex); + }); + } + + void _onPreviousPressed() { + if (!isFirstQuestion) { + setState(() { + currentQuestionIndex--; + }); + } + } + + void _onNextPressed() { + if (currentQuestion.isAnswered) { + currentQuestion.confirmSelection(); + if (isLastQuestion) { + context.navigateWithName(AppRoutes.possibleConditionsScreen); + } else { + setState(() { + currentQuestionIndex++; + }); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select an option before proceeding'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + } + } + + _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () => onConfirm(), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Triage".needTranslation, + // onLeadingTapped: () => _buildConfirmationBottomSheet( + // context: context, + // onConfirm: () => { + // context.pop(), + // context.pop(), + // }), + + leadingCallback: () => context.pop(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildQuestionCard(), + ], + ), + ), + ), + _buildStickyBottomCard(context), + ], + ), + ); + } + + Widget _buildQuestionCard() { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + )); + + return SlideTransition( + position: offsetAnimation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(currentQuestionIndex), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + currentQuestion.question.toText16(weight: FontWeight.w500), + SizedBox(height: 24.h), + ...List.generate(currentQuestion.options.length, (index) { + bool selected = currentQuestion.selectedOptionIndex == index; + return _buildOptionItem(index, selected, currentQuestion.options[index].text); + }), + ], + ), + ), + ); + } + + Widget _buildOptionItem(int index, bool selected, String optionText) { + return GestureDetector( + onTap: () => _onOptionSelected(index), + child: Container( + margin: EdgeInsets.only(bottom: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: selected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(5.r), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), + ), + child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded(child: optionText.toText14(weight: FontWeight.w500)), + ], + ), + ), + ); + } + + Widget _buildStickyBottomCard(BuildContext context) { + final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions); + final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore); + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: "Possible symptom: ".needTranslation, + style: TextStyle( + color: AppColors.greyTextColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + children: [ + TextSpan( + text: suggestedCondition, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + CustomRoundedProgressBar( + percentage: currentScore, + paddingBetween: 5.h, + color: AppColors.primaryRedColor, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), + height: 8.h, + titleWidget: RichText( + text: TextSpan( + text: "$currentScore% ", + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + children: [ + TextSpan( + text: "- Symptoms checker finding score".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + fontSize: 13.f, + ), + ), + ], + ), + ), + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: isFirstQuestion ? () {} : _onPreviousPressed, + isDisabled: isFirstQuestion, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation, + onPressed: _onNextPressed, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart new file mode 100644 index 0000000..fd00308 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.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'; +import 'package:provider/provider.dart'; + +class UserInfoSelectionScreen extends StatefulWidget { + const UserInfoSelectionScreen({super.key}); + + @override + State createState() => _UserInfoSelectionScreenState(); +} + +class _UserInfoSelectionScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeUserInfo(); + }); + } + + /// Initialize user info from appState if user is logged in + void _initializeUserInfo() { + final appState = getIt.get(); + final viewModel = context.read(); + + if (appState.isAuthenticated) { + final user = appState.getAuthenticatedUser(); + + if (user == null) return; + + // Populate gender (gender is int: 1=Male, 2=Female) + if (user.gender != null) { + String genderStr = user.gender == 1 + ? "Male" + : user.gender == 2 + ? "Female" + : "Other"; + viewModel.setGender(genderStr); + } + + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateTime.parse(user.dateofBirth!); + viewModel.setDateOfBirth(dob); + } catch (e) { + // If date parsing fails, ignore and let user fill manually + } + } + + // Note: AuthenticatedUser doesn't have height/weight fields + // User will need to fill these manually + } + // If not authenticated or fields are empty, user will fill them manually + } + + _buildEditInfoTile({ + required String leadingIcon, + required String title, + required String subTitle, + required VoidCallback onTap, + required String trailingIcon, + required BuildContext context, + Color? iconColor, + }) { + return InkWell( + onTap: onTap, + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.only(right: 10.h), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.greyColor), + child: Utils.buildSvgWithAssets(icon: leadingIcon, iconColor: iconColor)), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(weight: FontWeight.w500), + subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: trailingIcon, height: 24.h, width: 24.h), + ], + ), + ); + } + + Widget _getDivider() { + return Divider( + color: AppColors.dividerColor, + ).paddingSymmetrical(0, 16.h); + } + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + + String name = ""; + if (appState.isAuthenticated) { + name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + } else { + name = "Guest"; + } + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, viewModel, child) { + // Check if any field is empty + bool hasEmptyFields = viewModel.selectedGender == null || + viewModel.selectedAge == null || + viewModel.selectedHeight == null || + viewModel.selectedWeight == null; + + // Get display values + String genderText = viewModel.selectedGender ?? "Not set"; + // Show age calculated from DOB, not the DOB itself + String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; + String heightText = + viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; + String weightText = + viewModel.selectedWeight != null ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" : "Not set"; + + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Symptoms Checker".needTranslation, + isLeading: Navigator.canPop(context), + child: SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + children: [ + "Hello $name, Is your information up to date?".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + SizedBox(height: 24.h), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.genderIcon, + title: "Gender".needTranslation, + subTitle: genderText, + onTap: () { + viewModel.setUserInfoPage(0, isSinglePageEdit: true); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.calendarGrey, + title: "Age".needTranslation, + subTitle: ageText, + iconColor: AppColors.greyTextColor, + onTap: () { + viewModel.setUserInfoPage(1, isSinglePageEdit: true); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.rulerIcon, + title: "Height".needTranslation, + subTitle: heightText, + onTap: () { + viewModel.setUserInfoPage(2, isSinglePageEdit: true); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.weightScale, + title: "Weight".needTranslation, + subTitle: weightText, + onTap: () { + viewModel.setUserInfoPage(3, isSinglePageEdit: true); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + ], + ), + ), + ], + ).paddingAll(24.w), + ), + ), + ), + _buildBottomCard(context, hasEmptyFields), + ], + ); + }, + ), + ); + } + + Widget _buildBottomCard(BuildContext context, bool hasEmptyFields) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.all(8.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CustomButton( + text: "No, Edit all".needTranslation, + icon: AppAssets.edit_icon, + iconColor: AppColors.primaryRedColor, + onPressed: () { + context.read().setUserInfoPage(0, isSinglePageEdit: false); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Yes, It is".needTranslation, + icon: AppAssets.tickIcon, + iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + onPressed: hasEmptyFields + ? () {} // Empty function for disabled state + : () => context.navigateWithName(AppRoutes.organSelectorPage), + backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart new file mode 100644 index 0000000..d73f387 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -0,0 +1,47 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Age selection page content +class AgeSelectionPage extends StatelessWidget { + final int? selectedAge; + final Function(int) onAgeSelected; + + const AgeSelectionPage({ + super.key, + required this.selectedAge, + required this.onAgeSelected, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Consumer( + builder: (BuildContext context, symptomsViewModel, Widget? child) { + return Column( + children: [ + "What is your Date of Birth?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor).paddingAll(24.w), + SizedBox(height: 30.h), + ThreeColumnDatePicker( + enableHaptic: true, + enableSound: true, + initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1), + onDateChanged: (date) { + symptomsViewModel.setDateOfBirth(date); + log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}'); + }, + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart new file mode 100644 index 0000000..85cb6e2 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Gender selection page content +class GenderSelectionPage extends StatelessWidget { + final String? selectedGender; + final Function(String) onGenderSelected; + + GenderSelectionPage({ + super.key, + required this.selectedGender, + required this.onGenderSelected, + }); + + _buildGenderOption(String iconPng, String label, bool isSelected) { + return Container( + height: 160.h, + width: 160.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 24.r, side: isSelected ? BorderSide(color: AppColors.primaryRedColor, width: 2.5) : null), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildImgWithAssets(icon: iconPng, height: 80.h, width: 80.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + label.toText16( + weight: FontWeight.w500, + ) + ], + ), + ); + } + + final genders = ["Male", "Female"]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.all(24.w), + child: Consumer( + builder: (BuildContext context, symptomsViewModel, Widget? child) { + return Column( + children: [ + "What is your gender?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 70.h), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () => onGenderSelected(genders[0]), + child: _buildGenderOption(AppAssets.maleIcon, "Male".needTranslation, symptomsViewModel.selectedGender == genders[0]), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: InkWell( + onTap: () => onGenderSelected(genders[1]), + child: _buildGenderOption(AppAssets.femaleIcon, "Female".needTranslation, symptomsViewModel.selectedGender == genders[1]), + )) + ], + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart new file mode 100644 index 0000000..0744e81 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart @@ -0,0 +1,174 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Height selection page content +class HeightSelectionPage extends StatelessWidget { + const HeightSelectionPage({super.key}); + + Widget _unitSelector(SymptomsCheckerViewModel viewModel) { + return Container( + height: 54.h, + padding: EdgeInsets.all(4.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = (constraints.maxWidth - 8.w) / 2; + return Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: tabWidth, + height: constraints.maxHeight, + margin: EdgeInsets.only(left: viewModel.isHeightCm ? 0 : tabWidth + 8.w), + decoration: BoxDecoration( + color: AppColors.bottomNAVBorder, + borderRadius: BorderRadius.circular(7.r), + ), + ), + // Tab buttons + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + if (!viewModel.isHeightCm) { + final convertedHeight = viewModel.selectedHeight! * 30.48; + viewModel.setHeight(convertedHeight, true); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'CM', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + if (viewModel.isHeightCm) { + final convertedHeight = viewModel.selectedHeight! / 30.48; + viewModel.setHeight(convertedHeight, false); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'FT', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: !viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, child) { + // Define min/max values based on unit + final minValue = viewModel.isHeightCm ? 50.0 : 1.6; // 50cm or 1.6ft (approx. 1'7") + final maxValue = viewModel.isHeightCm ? 280.0 : 9.2; // 280cm or 9.2ft (approx. 9'2") + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Center( + child: Text( + 'How tall are you?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 24.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(viewModel), + ), + SizedBox(height: 20.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(flex: 1), + Expanded( + flex: 3, + child: Consumer( + builder: (context, viewModel, child) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: + viewModel.isHeightCm ? viewModel.selectedHeight?.round().toString() : viewModel.selectedHeight?.toStringAsFixed(1), + style: TextStyle( + fontSize: 90.f, + color: AppColors.textColor, + height: 1, + ), + ), + TextSpan( + text: viewModel.isHeightCm ? 'cm' : 'ft', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24.f, + ), + ), + ], + ), + ).paddingOnly(bottom: 100.h, left: 20.w); + }, + ), + ), + Expanded( + child: HeightScale( + enableHaptic: true, + enableSound: true, + minValue: minValue, + maxValue: maxValue, + initialHeight: viewModel.selectedHeight ?? 100, + isCm: viewModel.isHeightCm, + onHeightChanged: (newHeight) { + log("height: $newHeight"); + viewModel.setHeight(newHeight, viewModel.isHeightCm); + }, + ), + ), + ], + ), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart new file mode 100644 index 0000000..1d38a91 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart @@ -0,0 +1,162 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Weight selection page content +class WeightSelectionPage extends StatelessWidget { + final Function(double, bool) onWeightChanged; + + const WeightSelectionPage({super.key, required this.onWeightChanged}); + + Widget _unitSelector(SymptomsCheckerViewModel viewModel) { + bool isKg = viewModel.isWeightKg; + return Container( + height: 54.h, + padding: EdgeInsets.all(4.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = (constraints.maxWidth - 8.w) / 2; + return Stack( + children: [ + // Animated sliding indicator + AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: tabWidth, + height: constraints.maxHeight, + margin: EdgeInsets.only(left: isKg ? 0 : tabWidth + 8.w), + decoration: BoxDecoration( + color: AppColors.bottomNAVBorder, + borderRadius: BorderRadius.circular(7.r), + ), + ), + // Tab buttons + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + if (!isKg) { + // Convert from LBS to KG (1 lb = 0.453592 kg) + final convertedWeight = viewModel.selectedWeight! / 2.20462; + Future.microtask(() => onWeightChanged(convertedWeight, true)); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'KG', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + if (isKg) { + // Convert from KG to LBS (1 kg = 2.20462 lbs) + final convertedWeight = viewModel.selectedWeight! * 2.20462; + Future.microtask(() => onWeightChanged(convertedWeight, false)); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'LBS', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: !isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, viewModel, child) { + bool isKg = viewModel.isWeightKg; + // Define min/max values based on unit + final minValue = isKg ? 10.0 : 22.0; // 10kg or 22lbs + final maxValue = isKg ? 200.0 : 440.0; // 200kg or 440lbs + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Center( + child: Text( + 'What is your weight?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 24.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(viewModel), + ), + SizedBox(height: 60.h), + // Weight display centered + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + viewModel.selectedWeight!.round().toString(), + style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + ), + SizedBox(width: 8.w), + Text( + isKg ? 'kg' : 'lbs', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f), + ).paddingOnly(bottom: 10.h), + ], + ), + ), + SizedBox(height: 40.h), + // Horizontal weight picker + WeightScale( + enableHaptic: true, + enableSound: true, + minValue: minValue, + maxValue: maxValue, + initialWeight: viewModel.selectedWeight!, + isKg: isKg, + onWeightChanged: (newWeight) { + log("weight: $newWeight"); + Future.microtask(() => onWeightChanged(newWeight, isKg)); + }, + ), + ], + ); + }); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart new file mode 100644 index 0000000..523470e --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -0,0 +1,276 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.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'; +import 'package:provider/provider.dart'; + +/// Manages the user info selection flow with PageView +/// Only the page content changes, header and footer remain fixed +class UserInfoFlowManager extends StatefulWidget { + const UserInfoFlowManager({super.key}); + + @override + State createState() => _UserInfoFlowManagerState(); +} + +class _UserInfoFlowManagerState extends State { + late PageController _pageController; + late SymptomsCheckerViewModel _viewModel; + + // Page titles + final List _pageTitles = [ + "Your Gender", + "Your Birth Date", + "Your Height", + "Your Weight", + ]; + + @override + void initState() { + super.initState(); + _viewModel = context.read(); + // Initialize PageController with the current page from ViewModel + _pageController = PageController(initialPage: _viewModel.userInfoCurrentPage); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onNext() { + // If in single page edit mode, just save and go back + if (_viewModel.isSinglePageEditMode) { + context.pop(); + return; + } + + // Otherwise, continue with normal flow + if (_viewModel.userInfoCurrentPage < 3) { + _viewModel.nextUserInfoPage(); + _pageController.animateToPage( + _viewModel.userInfoCurrentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + _submitUserInfo(); + } + } + + void _onPrevious() { + if (_viewModel.userInfoCurrentPage > 0) { + _viewModel.previousUserInfoPage(); + _pageController.animateToPage( + _viewModel.userInfoCurrentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + context.pop(); + } + } + + void _submitUserInfo() { + final userInfo = _viewModel.getUserInfoData(); + + // Log user info + log('User Info Submitted:'); + log('Gender: ${userInfo['gender']}'); + log('Age: ${userInfo['age']}'); + log('Height: ${userInfo['height']} ${userInfo['heightUnit']}'); + log('Weight: ${userInfo['weight']} ${userInfo['weightUnit']}'); + + // TODO: Save user info to backend/storage + + // Navigate to symptoms checker or next screen + context.pop(); + } + + Widget _buildProgressBar() { + return Consumer( + builder: (BuildContext context, viewModel, child) { + return Row( + children: List.generate(4, (index) { + final isCompleted = index < viewModel.userInfoCurrentPage; + final isCurrentStep = index == viewModel.userInfoCurrentPage; + + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Stack( + children: [ + // Background (grey) + Container( + height: 4.h, + decoration: BoxDecoration( + color: AppColors.greyLightColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + // Animated red fill from left to right + TweenAnimationBuilder( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + tween: Tween( + begin: 0.0, + end: isCompleted + ? 1.0 + : isCurrentStep + ? 1.0 + : 0.0, + ), + builder: (context, value, child) { + return FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: value, + child: Container( + height: 4.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + borderRadius: BorderRadius.circular(8.r), + boxShadow: isCurrentStep && value > 0 + ? [ + BoxShadow( + color: AppColors.primaryRedColor.withValues(alpha: 0.4), + blurRadius: 8.r, + spreadRadius: 1.r, + ), + ] + : null, + ), + ), + ); + }, + ), + ], + ), + ), + ), + ); + }), + ); + }, + ); + } + + Widget _buildStickyBottomCard() { + return Consumer(builder: (BuildContext context, viewModel, child) { + bool isLastPage = viewModel.isUserInfoLastPage; + bool isFirstPage = viewModel.isUserInfoFirstPage; + bool isSingleEdit = viewModel.isSinglePageEditMode; + + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), + ), + padding: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h), + child: SafeArea( + top: false, + child: isSingleEdit + ? // Single page edit mode - show only Save button + CustomButton( + text: "Save".needTranslation, + onPressed: _onNext, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ) + : // Complete flow mode - show Previous/Next buttons + Row( + children: [ + if (!isFirstPage) ...[ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPrevious, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + ], + Expanded( + child: CustomButton( + text: isLastPage ? "Submit".needTranslation : "Next".needTranslation, + onPressed: _onNext, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ), + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + physics: NeverScrollableScrollPhysics(), + title: _pageTitles[_viewModel.userInfoCurrentPage].needTranslation, + isLeading: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + // Only show progress bar in complete flow mode + if (!_viewModel.isSinglePageEditMode) _buildProgressBar(), + if (!_viewModel.isSinglePageEditMode) SizedBox(height: 24.h), + SizedBox( + height: 600.h, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), // Disable swipe + onPageChanged: (index) { + _viewModel.setUserInfoPage(index); + }, + children: [ + GenderSelectionPage( + selectedGender: _viewModel.selectedGender, + onGenderSelected: _viewModel.setGender, + ), + AgeSelectionPage( + selectedAge: _viewModel.selectedAge, + onAgeSelected: _viewModel.setAge, + ), + HeightSelectionPage(), + WeightSelectionPage(onWeightChanged: _viewModel.setWeight), + ], + ), + ), + ], + ), + ), + ), + _buildStickyBottomCard(), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart new file mode 100644 index 0000000..98051e0 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart @@ -0,0 +1,235 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class ThreeColumnDatePicker extends StatefulWidget { + final DateTime initialDate; + final ValueChanged? onDateChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const ThreeColumnDatePicker({ + super.key, + required this.initialDate, + this.onDateChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _ThreeColumnDatePickerState(); +} + +class _ThreeColumnDatePickerState extends State { + static const int yearRange = 100; + static const double _defaultItemExtent = 48.0; // will be scaled with .h + + late final List _days; + late final List _months; + late final List _years; + + late FixedExtentScrollController _dayController; + late FixedExtentScrollController _monthController; + late FixedExtentScrollController _yearController; + + int _selectedDay = 0; + int _selectedMonth = 0; + int _selectedYearIndex = 0; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + double get _itemExtent => _defaultItemExtent.h; + + @override + void initState() { + super.initState(); + + _days = List.generate(31, (i) => (i + 1).toString().padLeft(2, '0')); + _months = const ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + final currentYear = DateTime.now().year; + _years = List.generate(yearRange, (i) => currentYear - i); + + _selectedDay = (widget.initialDate.day - 1).clamp(0, _days.length - 1); + _selectedMonth = (widget.initialDate.month - 1).clamp(0, _months.length - 1); + _selectedYearIndex = _years.indexOf(widget.initialDate.year); + if (_selectedYearIndex == -1) _selectedYearIndex = 0; + + _dayController = FixedExtentScrollController(initialItem: _selectedDay); + _monthController = FixedExtentScrollController(initialItem: _selectedMonth); + _yearController = FixedExtentScrollController(initialItem: _selectedYearIndex); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _dayController.dispose(); + _monthController.dispose(); + _yearController.dispose(); + super.dispose(); + } + + void _emitDate() { + final day = int.parse(_days[_selectedDay]); + final month = _selectedMonth + 1; + final year = _years[_selectedYearIndex]; + final date = DateTime(year, month, day); + widget.onDateChanged?.call(date); + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + Widget _wheel({ + required FixedExtentScrollController controller, + required int itemCount, + required Widget Function(int index, bool selected) itemBuilder, + required ValueChanged onSelectedItemChanged, + required int currentlySelectedIndex, + }) { + return Expanded( + child: SizedBox( + height: _itemExtent * 5, // show ~5 rows + child: ListWheelScrollView.useDelegate( + controller: controller, + itemExtent: _itemExtent, + physics: const BouncingScrollPhysics(), + diameterRatio: 2.2, + squeeze: 1.2, + perspective: 0.004, + // overAndUnderCenterOpacity: 0.6, + onSelectedItemChanged: (i) { + // update selected index, emit date and schedule feedback + onSelectedItemChanged(i); + _scheduleFeedback(); + }, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + if (index < 0 || index >= itemCount) return null; + final bool selected = index == currentlySelectedIndex; + return Center(child: itemBuilder(index, selected)); + }, + childCount: itemCount, + ), + ), + ), + ); + } + + Widget _styledText(String text, bool selected) { + return Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: selected ? 22.f : 20.f, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? AppColors.textColor : AppColors.greyTextColor.withValues(alpha: 0.9), + height: 1.0, + letterSpacing: selected ? -0.02 * 30 : -0.02 * 18, + ), + ); + } + + @override + Widget build(BuildContext context) { + final pickerHeight = _itemExtent * 5; + final pointerSize = 20.w; + final pointerTop = (pickerHeight / 2) - (pointerSize / 2); + + return LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: pickerHeight, + child: Stack( + children: [ + Row( + children: [ + // Day wheel + _wheel( + controller: _dayController, + itemCount: _days.length, + currentlySelectedIndex: _selectedDay, + onSelectedItemChanged: (i) { + setState(() => _selectedDay = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_days[index], selected), + ), + + // Month wheel + _wheel( + controller: _monthController, + itemCount: _months.length, + currentlySelectedIndex: _selectedMonth, + onSelectedItemChanged: (i) { + setState(() => _selectedMonth = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_months[index], selected), + ), + + // Year wheel + _wheel( + controller: _yearController, + itemCount: _years.length, + currentlySelectedIndex: _selectedYearIndex, + onSelectedItemChanged: (i) { + setState(() => _selectedYearIndex = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_years[index].toString(), selected), + ), + ], + ), + + // subtle center overlay (optional — keeps layout consistent & highlights center row) + Positioned.fill( + child: IgnorePointer( + child: Center( + child: SizedBox( + height: _itemExtent, + ), + ), + ), + ), + + // left red triangular pointer aligned to center row + Positioned( + left: 0.w, + top: pointerTop, + child: TriangleIndicator( + pointerSize: pointerSize, + // your TriangleIndicator supports direction param; use left as in the original + // if your TriangleIndicator doesn't accept direction, remove the param + direction: TriangleDirection.left, + ), + ), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart new file mode 100644 index 0000000..29baf64 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HeightScale extends StatefulWidget { + final double minValue; + final double maxValue; + final double initialHeight; + final bool isCm; + final ValueChanged? onHeightChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const HeightScale({ + super.key, + required this.minValue, + required this.maxValue, + required this.initialHeight, + required this.isCm, + this.onHeightChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _HeightScaleState(); +} + +class _HeightScaleState extends State { + late FixedExtentScrollController _scrollController; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + // Get increment based on unit (CM = 1.0, FT = 0.1) + double get _increment => widget.isCm ? 1.0 : 0.1; + + @override + void initState() { + super.initState(); + int initialIndex = ((widget.initialHeight - widget.minValue) / _increment).round(); + _scrollController = FixedExtentScrollController(initialItem: initialIndex); + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final pointerSize = 20.w; + final pickerHeight = 300.h; + final pointerTop = (pickerHeight / 2) - (pointerSize / 2); + + return SizedBox( + height: pickerHeight, + child: Stack( + children: [ + // Scrollable wheel picker + ListWheelScrollView.useDelegate( + controller: _scrollController, + itemExtent: 10.h, + diameterRatio: 2.0, + squeeze: 1.2, + perspective: 0.001, + physics: const BouncingScrollPhysics(), + onSelectedItemChanged: (index) { + final selectedValue = widget.minValue + (index * _increment); + widget.onHeightChanged?.call(selectedValue); + _scheduleFeedback(); + }, + childDelegate: ListWheelChildBuilderDelegate( + childCount: ((widget.maxValue - widget.minValue) / _increment).round() + 1, + builder: (context, index) { + final height = widget.minValue + (index * _increment); + + // For CM: main mark every 10, mid mark every 5 + // For FT: main mark every 1.0 (10 ticks), mid mark every 0.5 (5 ticks) + final isMainMark = widget.isCm ? height % 10 == 0 : (height * 10).round() % 10 == 0; + final isMidMark = widget.isCm ? height % 5 == 0 : (height * 10).round() % 5 == 0; + + return SizedBox( + width: 100.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Number label for main marks + if (isMainMark) + SizedBox( + width: 30.w, + child: Text( + widget.isCm ? height.round().toString() : height.toStringAsFixed(1), + style: TextStyle( + fontSize: 11.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + height: 1, + ), + textAlign: TextAlign.right, + ), + ) + else + SizedBox(width: 30.w), + SizedBox(width: 4.w), + // Ruler mark + Container( + width: isMainMark + ? 40.w + : isMidMark + ? 30.w + : 25.w, + height: isMainMark || isMidMark ? 2.5.h : 1.5.h, + decoration: BoxDecoration( + color: isMainMark + ? AppColors.textColor + : isMidMark + ? AppColors.textColorLight + : AppColors.textColorLight.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2.r), + ), + ), + ], + ), + ); + }, + ), + ), + // Triangle indicator pointing to selected value + Positioned( + right: 0, + top: pointerTop, + child: TriangleIndicator(pointerSize: pointerSize), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart new file mode 100644 index 0000000..429279d --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +enum TriangleDirection { left, right, up, down } + +class TriangleIndicator extends StatelessWidget { + final double pointerSize; + final Color? color; + final TriangleDirection direction; + + const TriangleIndicator({ + super.key, + required this.pointerSize, + this.color, + this.direction = TriangleDirection.right, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(pointerSize, pointerSize), + painter: _TrianglePainter( + color: color ?? AppColors.primaryRedColor, + direction: direction, + ), + ); + } +} + +class _TrianglePainter extends CustomPainter { + final Color color; + final TriangleDirection direction; + + _TrianglePainter({required this.color, required this.direction}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final path = Path(); + final w = size.width; + final h = size.height; + + switch (direction) { + case TriangleDirection.right: + // apex on the right, base on the left + path.moveTo(0, h / 2); + path.lineTo(w, 0); + path.lineTo(w, h); + path.close(); + break; + case TriangleDirection.left: + // apex on the left, base on the right + path.moveTo(w, h / 2); + path.lineTo(0, 0); + path.lineTo(0, h); + path.close(); + break; + case TriangleDirection.up: + // apex on top, base on bottom + path.moveTo(w / 2, 0); + path.lineTo(0, h); + path.lineTo(w, h); + path.close(); + break; + case TriangleDirection.down: + // apex on bottom, base on top + path.moveTo(w / 2, h); + path.lineTo(0, 0); + path.lineTo(w, 0); + path.close(); + break; + } + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _TrianglePainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.direction != direction; + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart new file mode 100644 index 0000000..8526a52 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WeightScale extends StatefulWidget { + final double minValue; + final double maxValue; + final double initialWeight; + final bool isKg; + final ValueChanged? onWeightChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const WeightScale({ + super.key, + required this.minValue, + required this.maxValue, + required this.initialWeight, + required this.isKg, + this.onWeightChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _WeightScaleState(); +} + +class _WeightScaleState extends State { + late ScrollController _scrollController; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + int? _lastReportedIndex; + final double _itemWidth = 8.0; // Width per weight unit + + @override + void initState() { + super.initState(); + int initialIndex = (widget.initialWeight - widget.minValue).round(); + final initialOffset = initialIndex * _itemWidth; + _scrollController = ScrollController(initialScrollOffset: initialOffset); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (!_scrollController.hasClients) return; + + final offset = _scrollController.offset; + final index = (offset / _itemWidth).round(); + final maxIndex = (widget.maxValue - widget.minValue).round(); + + if (index != _lastReportedIndex && index >= 0 && index <= maxIndex) { + _lastReportedIndex = index; + final selectedValue = widget.minValue + index; + widget.onWeightChanged?.call(selectedValue); + _scheduleFeedback(); + } + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final pointerSize = 20.h; + final pickerHeight = 100.h; + final itemCount = (widget.maxValue - widget.minValue).round() + 1; + + return SizedBox( + height: pickerHeight, + child: Stack( + alignment: Alignment.center, + children: [ + // Horizontal scrollable ruler with gradient fade + ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 2), + itemCount: itemCount, + itemBuilder: (context, index) { + final weight = (widget.minValue + index).round(); + final isMainMark = weight % 10 == 0; + final isMidMark = weight % 5 == 0; + + return SizedBox( + width: _itemWidth, + child: Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + // Ruler mark (vertical line) + Positioned( + bottom: 0, + child: Container( + width: isMainMark || isMidMark ? 2.5.w : 1.5.w, + height: isMainMark + ? 40.h + : isMidMark + ? 30.h + : 25.h, + decoration: BoxDecoration( + color: isMainMark + ? AppColors.textColor + : isMidMark + ? AppColors.textColorLight + : AppColors.textColorLight.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2.r), + ), + ), + ), + // Number label for main marks + if (isMainMark) + Positioned( + bottom: 45.h, + child: Text( + weight.toString(), + style: TextStyle( + fontSize: 11.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + height: 1, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.visible, + ), + ), + ], + ), + ); + }, + ), + // Triangle indicator pointing to selected value + Positioned( + bottom: 0, + child: TriangleIndicator( + pointerSize: pointerSize, + direction: TriangleDirection.up, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/condition_card.dart b/lib/presentation/symptoms_checker/widgets/condition_card.dart new file mode 100644 index 0000000..87a8f3d --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/condition_card.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class ConditionCard extends StatelessWidget { + final IconData icon; + final String title; + final int percentage; + final String tagText; + final String clinic; + final List symptoms; + final String description; + final String? monitorNote; + final String? appointmentLabel; + final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum; + final VoidCallback? onActionPressed; + + const ConditionCard({ + super.key, + required this.icon, + required this.title, + required this.percentage, + required this.tagText, + required this.clinic, + required this.symptoms, + required this.description, + this.monitorNote, + this.appointmentLabel, + required this.possibleConditionsSeverityEnum, + this.onActionPressed, + }); + + Color getChipColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) { + switch (possibleConditionsSeverityEnum) { + case PossibleConditionsSeverityEnum.seekMedicalAdvice: + return AppColors.chipColorSeekMedicalAdvice; + case PossibleConditionsSeverityEnum.monitorOnly: + return AppColors.chipColorMonitor; + case PossibleConditionsSeverityEnum.emergency: + return AppColors.chipColorEmergency; + } + } + + Color getChipTextColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) { + switch (possibleConditionsSeverityEnum) { + case PossibleConditionsSeverityEnum.seekMedicalAdvice: + return AppColors.chipTextColorSeekMedicalAdvice; + case PossibleConditionsSeverityEnum.monitorOnly: + return AppColors.chipColorMonitor; + case PossibleConditionsSeverityEnum.emergency: + return AppColors.chipColorEmergency; + } + } + + _buildSymptomsRow() { + return Wrap( + spacing: 6.w, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (int i = 0; i < symptoms.length; i++) ...[ + Text(symptoms[i], style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)), + if (i != symptoms.length - 1) + Padding( + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Icon(Icons.circle, size: 7, color: AppColors.textColor), + ), + ], + ], + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48.w, + height: 48.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2), + borderRadius: 12.r, + hasShadow: false, + ), + child: Icon(icon, color: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum), size: 24.f), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + overflow: TextOverflow.ellipsis, + ), + ), + AppCustomChipWidget( + labelText: tagText, + backgroundColor: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2), + textColor: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum), + ), + ], + ), + CustomRoundedProgressBar( + percentage: percentage, + height: 6.h, + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum), + backgroundColor: AppColors.scaffoldBgColor, + titleWidget: Row( + children: [ + Text( + "$percentage%", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12.f, + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum), + ), + ), + ], + ).paddingSymmetrical(0, 4.h), + ), + ], + ), + ), + ], + ).paddingAll(16.w), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + clinic, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14.f, + color: AppColors.textColor, + ), + ), + _buildSymptomsRow(), + SizedBox(height: 16.h), + Text("Description".needTranslation, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.f, color: AppColors.textColor)), + SizedBox(height: 2.h), + Text(description, style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)), + if (possibleConditionsSeverityEnum == PossibleConditionsSeverityEnum.emergency) + CustomButton( + text: appointmentLabel ?? "Book Appointment".needTranslation, + onPressed: () { + if (onActionPressed != null) { + onActionPressed!(); + } + }, + backgroundColor: AppColors.lightRedButtonColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 48.h, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 18.h, + ).paddingOnly(top: 16.w), + if (monitorNote != null) + Container( + margin: EdgeInsets.only(top: 12.h), + child: AppCustomChipWidget( + labelText: monitorNote!, + backgroundColor: AppColors.whiteColor, + textColor: AppColors.textColor, + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + ), + ), + ], + ).paddingAll(16.w), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart new file mode 100644 index 0000000..d09c244 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; + +class CustomRoundedProgressBar extends StatelessWidget { + final int percentage; + final Color color; + final Color backgroundColor; + final double? height; + final double? paddingBetween; + final Widget? titleWidget; + + const CustomRoundedProgressBar({ + super.key, + this.titleWidget, + required this.percentage, + this.paddingBetween, + required this.color, + required this.backgroundColor, + this.height, + }); + + @override + Widget build(BuildContext context) { + final h = height ?? 8.h; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (titleWidget != null) ...[ + titleWidget!, + if (paddingBetween != null) ...[ + SizedBox(height: paddingBetween), + ] + ], + LayoutBuilder( + builder: (context, constraints) { + double radius = h / 2; + return Container( + height: h, + width: constraints.maxWidth, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(radius), + ), + child: Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: (percentage.clamp(0, 100)) / 100, + child: Container( + height: height, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.horizontal( + left: Radius.circular(radius), + right: Radius.circular(radius), + ), + ), + ), + ), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart new file mode 100644 index 0000000..8957fb1 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart @@ -0,0 +1,134 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart'; + +class InteractiveBodyWidget extends StatefulWidget { + final String bodyImageAsset; + final List organs; + final Set selectedOrganIds; + final bool isBodyHidden; + final Function(String organId) onOrganTap; + final String? tooltipOrganId; + + const InteractiveBodyWidget({ + super.key, + required this.bodyImageAsset, + required this.organs, + required this.selectedOrganIds, + required this.onOrganTap, + this.isBodyHidden = false, + this.tooltipOrganId, + }); + + @override + State createState() => _InteractiveBodyWidgetState(); +} + +class _InteractiveBodyWidgetState extends State { + double? _imageAspectRatio; + final TransformationController _transformationController = TransformationController(); + + @override + void initState() { + super.initState(); + _loadImageAspectRatio(); + } + + @override + void didUpdateWidget(InteractiveBodyWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.bodyImageAsset != widget.bodyImageAsset) { + _loadImageAspectRatio(); + // Reset zoom when switching views + _transformationController.value = Matrix4.identity(); + } + } + + @override + void dispose() { + _transformationController.dispose(); + super.dispose(); + } + + Future _loadImageAspectRatio() async { + final ByteData data = await rootBundle.load(widget.bodyImageAsset); + final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + final image = frameInfo.image; + + if (mounted) { + setState(() { + _imageAspectRatio = image.width / image.height; + }); + } + } + + @override + Widget build(BuildContext context) { + if (_imageAspectRatio == null) { + return const Center(child: CircularProgressIndicator()); + } + + return LayoutBuilder( + builder: (context, constraints) { + return Center( + child: InteractiveViewer( + transformationController: _transformationController, + minScale: 0.5, + maxScale: 4.0, + clipBehavior: Clip.none, + child: AspectRatio( + aspectRatio: _imageAspectRatio!, + child: LayoutBuilder( + builder: (context, imageConstraints) { + return Stack( + clipBehavior: Clip.none, + children: [ + // Body image + Positioned.fill( + child: Image.asset( + widget.bodyImageAsset, + fit: BoxFit.fill, + color: widget.isBodyHidden ? Colors.black.withValues(alpha: 0.5) : null, + ), + ), + + // Organ dots + ...widget.organs.map((organ) { + final isSelected = widget.selectedOrganIds.contains(organ.id); + final dotSize = 16.0; + + final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2); + final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2); + + return Positioned( + left: leftPos, + top: topPos, + child: OrganDot(isSelected: isSelected, onTap: () => widget.onOrganTap(organ.id), size: dotSize), + ); + }), + + // Tooltip + if (widget.tooltipOrganId != null) + ...widget.organs.where((organ) => organ.id == widget.tooltipOrganId).map( + (organ) { + final centerX = organ.position.x * imageConstraints.maxWidth; + final centerY = organ.position.y * imageConstraints.maxHeight; + return OrganTooltipWidget(organName: organ.description, position: Offset(centerX, centerY)); + }, + ), + ], + ); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart b/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart new file mode 100644 index 0000000..0783dfb --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class OrganDot extends StatelessWidget { + final bool isSelected; + final VoidCallback onTap; + final double size; + + const OrganDot({super.key, required this.isSelected, required this.onTap, this.size = 50.0}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: size.w, + height: size.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? AppColors.whiteColor : AppColors.blackColor.withValues(alpha: 0.6), + ), + child: Padding( + padding: EdgeInsets.all(isSelected ? 2.w : 3.w), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor, + ), + child: isSelected + ? Center( + child: Container( + width: size.w * 0.25, + height: size.h * 0.25, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.whiteColor, + ), + ), + ) + : null, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart b/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart new file mode 100644 index 0000000..6c2625b --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class OrganTooltipWidget extends StatelessWidget { + final String organName; + final Offset position; + + const OrganTooltipWidget({super.key, required this.organName, required this.position}); + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + child: FractionalTranslation( + translation: const Offset(-0.5, -1.2), // Center horizontally, position above the dot + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + borderRadius: 8.h, + hasShadow: true, + ), + child: Text( + organName, + style: TextStyle( + color: AppColors.textColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart new file mode 100644 index 0000000..c0f1be3 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class SelectedOrgansSection extends StatefulWidget { + final List selectedOrgans; + final Function(String organId) onRemoveOrgan; + + const SelectedOrgansSection({ + super.key, + required this.selectedOrgans, + required this.onRemoveOrgan, + }); + + @override + State createState() => _SelectedOrgansSectionState(); +} + +class _SelectedOrgansSectionState extends State { + bool _isExpanded = true; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: AppColors.blackColor.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Selected Organs'.needTranslation, + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + Row( + children: [ + if (widget.selectedOrgans.isNotEmpty) + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '${widget.selectedOrgans.length}', + style: TextStyle( + color: AppColors.whiteColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(width: 8.w), + AnimatedRotation( + turns: _isExpanded ? 0.5 : 0.0, + duration: const Duration(milliseconds: 200), + child: Icon( + Icons.keyboard_arrow_down, + color: AppColors.textColorLight, + size: 24.h, + ), + ), + ], + ), + ], + ), + ), + if (_isExpanded) ...[ + SizedBox(height: 16.h), + if (widget.selectedOrgans.isEmpty) + Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: Text( + 'No organs selected yet'.needTranslation, + style: TextStyle( + color: AppColors.greyTextColor, + fontSize: 14.f, + ), + ), + ) + else + Wrap( + spacing: 8.w, + runSpacing: 8.h, + children: widget.selectedOrgans.map((organ) { + return AppCustomChipWidget( + labelText: organ.description, + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + deleteIcon: AppAssets.cross_circle, + deleteIconColor: AppColors.primaryRedColor, + deleteIconHasColor: true, + onDeleteTap: () => widget.onRemoveOrgan(organ.id), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + ); + }).toList(), + ), + ], + ], + ), + ); + } +} diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart new file mode 100644 index 0000000..59b5ce4 --- /dev/null +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -0,0 +1,1838 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart'; +import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_camera_device.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_cmd_channel.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_live_transcription_message_info.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_share_action.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/utils/jwt.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/video_view.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:image_picker/image_picker.dart'; + +class CallScreen extends StatefulHookWidget { + const CallScreen({Key? key}) : super(key: key); + + @override + State createState() => _CallScreenState(); +} + +class _CallScreenState extends State { + static TextEditingController changeNameController = TextEditingController(); + double opacityLevel = 1.0; + + final voIPKit = FlutterIOSVoIPKit.instance; + + bool isScreenBeingShared = false; + + void _changeOpacity() { + setState(() => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0); + } + + @override + Widget build(BuildContext context) { + var zoom = ZoomVideoSdk(); + var eventListener = ZoomVideoSdkEventListener(); + var isInSession = useState(false); + var sessionName = useState(''); + var sessionPassword = useState(''); + var users = useState([]); + var fullScreenUser = useState(null); + var sharingUser = useState(null); + var videoInfo = useState(''); + var isSharing = useState(false); + var isMuted = useState(true); + var isVideoOn = useState(false); + var isSpeakerOn = useState(false); + var isRenameModalVisible = useState(false); + var isRecordingStarted = useState(false); + var isMicOriginalOn = useState(false); + var isMounted = useIsMounted(); + var audioStatusFlag = useState(false); + var videoStatusFlag = useState(false); + var userNameFlag = useState(false); + var userShareStatusFlag = useState(false); + var isReceiveSpokenLanguageContentEnabled = useState(false); + var isVideoMirrored = useState(false); + var isOriginalAspectRatio = useState(false); + var isPiPView = useState(false); + var isSharedCamera = useState(false); + CameraShareView cameraShareView = const CameraShareView(creationParams: {}); + + //hide status bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); + var circleButtonSize = 65.0; + Color backgroundColor = const Color(0xFF232323); + Color buttonBackgroundColor = const Color.fromRGBO(0, 0, 0, 0.6); + Color chatTextColor = const Color(0xFFAAAAAA); + Widget changeNamePopup; + final args = ModalRoute.of(context)!.settings.arguments as CallArguments; + + useEffect(() { + Future.microtask(() async { + var token = generateJwt(args.sessionName, args.role); + + Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); + + try { + Map SDKaudioOptions = {"connect": true, "mute": false, "autoAdjustSpeakerVolume": false}; + Map SDKvideoOptions = { + "localVideoOn": true, + }; + JoinSessionConfig joinSession = JoinSessionConfig( + sessionName: args.sessionName, + sessionPassword: args.sessionPwd, + token: token, + userName: args.displayName, + audioOptions: SDKaudioOptions, + videoOptions: SDKvideoOptions, + sessionIdleTimeoutMins: int.parse(args.sessionIdleTimeoutMins), + ); + await zoom.joinSession(joinSession); + } catch (e) { + const AlertDialog( + title: Text("Error"), + content: Text("Failed to join the session"), + ); + Future.delayed(const Duration(milliseconds: 1000)).asStream().listen((event) { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + } + }); + return null; + }, []); + + useEffect(() { + final sessionJoinListener = eventListener.addListener(EventType.onSessionJoin, (data) async { + data = data as Map; + isInSession.value = true; + zoom.session.getSessionName().then((value) => sessionName.value = value!); + sessionPassword.value = await zoom.session.getSessionPassword(); + debugPrint("sessionPhonePasscode: ${await zoom.session.getSessionPhonePasscode()}"); + ZoomVideoSdkUser mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(data['sessionUser'])); + List? remoteUsers = await zoom.session.getRemoteUsers(); + var muted = await mySelf.audioStatus?.isMuted(); + var videoOn = await mySelf.videoStatus?.isOn(); + var speakerOn = await zoom.audioHelper.getSpeakerStatus(); + fullScreenUser.value = mySelf; + remoteUsers?.insert(0, mySelf); + isMuted.value = muted!; + isSpeakerOn.value = speakerOn; + isVideoOn.value = videoOn!; + users.value = remoteUsers!; + isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(); + }); + + final sessionLeaveListener = eventListener.addListener(EventType.onSessionLeave, (data) async { + data = data as Map; + debugPrint("onSessionLeave: ${data['reason']}"); + isInSession.value = false; + users.value = []; + fullScreenUser.value = null; + // Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + + final sessionNeedPasswordListener = eventListener.addListener(EventType.onSessionNeedPassword, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Need Password'), + content: const Text('Password is required'), + actions: [ + TextButton( + onPressed: () async => { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final sessionPasswordWrongListener = eventListener.addListener(EventType.onSessionPasswordWrong, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Password Incorrect'), + content: const Text('Password is wrong'), + actions: [ + TextButton( + onPressed: () async => { + Navigator.popAndPushNamed(context, 'Join', arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final userVideoStatusChangedListener = eventListener.addListener(EventType.onUserVideoStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.videoStatus?.isOn().then((on) => isVideoOn.value = on); + } + } + } + videoStatusFlag.value = !videoStatusFlag.value; + }); + + final userAudioStatusChangedListener = eventListener.addListener(EventType.onUserAudioStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + } + } + } + audioStatusFlag.value = !audioStatusFlag.value; + }); + + final userShareStatusChangeListener = eventListener.addListener(EventType.onUserShareStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString())); + ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction'])); + + if (shareAction.shareStatus == ShareStatus.Start || shareAction.shareStatus == ShareStatus.Resume) { + sharingUser.value = shareUser; + fullScreenUser.value = shareUser; + isSharing.value = (shareUser.userId == mySelf?.userId); + + setState(() { + isScreenBeingShared = true; + }); + Future.delayed(const Duration(seconds: 2), () { + setState(() { + isScreenBeingShared = false; + }); + }); + } else { + sharingUser.value = null; + isSharing.value = false; + isSharedCamera.value = false; + fullScreenUser.value = mySelf; + isScreenBeingShared = false; + setState(() { + isScreenBeingShared = true; + }); + Future.delayed(const Duration(seconds: 2), () { + setState(() { + isScreenBeingShared = false; + }); + }); + } + userShareStatusFlag.value = !userShareStatusFlag.value; + }); + + final shareContentChangedListener = eventListener.addListener(EventType.onShareContentChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString())); + ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction'])); + if (shareAction.shareType == ShareType.Camera) { + debugPrint("Camera share started"); + isSharedCamera.value = (shareUser.userId == mySelf?.userId); + } + }); + + final userJoinListener = eventListener.addListener(EventType.onUserJoin, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['remoteUsers']) as List; + List remoteUserList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + remoteUserList.insert(0, mySelf!); + users.value = remoteUserList; + }); + + final userLeaveListener = eventListener.addListener(EventType.onUserLeave, (data) async { + if (!isMounted()) return; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + data = data as Map; + List? remoteUserList = await zoom.session.getRemoteUsers(); + var leftUserListJson = jsonDecode(data['leftUsers']) as List; + List leftUserLis = leftUserListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + if (fullScreenUser.value != null) { + for (var user in leftUserLis) { + { + if (fullScreenUser.value?.userId == user.userId) { + fullScreenUser.value = mySelf; + } + } + } + } else { + fullScreenUser.value = mySelf; + } + remoteUserList?.add(mySelf!); + users.value = remoteUserList!; + }); + + final userNameChangedListener = eventListener.addListener(EventType.onUserNameChanged, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + int index; + for (var user in users.value) { + if (user.userId == changedUser.userId) { + index = users.value.indexOf(user); + users.value[index] = changedUser; + } + } + userNameFlag.value = !userNameFlag.value; + }); + + final commandReceived = eventListener.addListener(EventType.onCommandReceived, (data) async { + data = data as Map; + debugPrint("sender: ${ZoomVideoSdkUser.fromJson(jsonDecode(data['sender']))}, command: ${data['command']}"); + }); + + final liveStreamStatusChangeListener = eventListener.addListener(EventType.onLiveStreamStatusChanged, (data) async { + data = data as Map; + debugPrint("onLiveStreamStatusChanged: status: ${data['status']}"); + }); + + final liveTranscriptionStatusChangeListener = eventListener.addListener(EventType.onLiveTranscriptionStatus, (data) async { + data = data as Map; + debugPrint("onLiveTranscriptionStatus: status: ${data['status']}"); + }); + + final cloudRecordingStatusListener = eventListener.addListener(EventType.onCloudRecordingStatus, (data) async { + data = data as Map; + debugPrint("onCloudRecordingStatus: status: ${data['status']}"); + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (data['status'] == RecordingStatus.Start) { + if (mySelf != null && !mySelf.isHost!) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: const Text('The session is being recorded.'), + actions: [ + TextButton( + onPressed: () async { + await zoom.acceptRecordingConsent(); + if (context.mounted) { + Navigator.pop(context); + } + ; + }, + child: const Text('accept'), + ), + TextButton( + onPressed: () async { + String currentConsentType = await zoom.getRecordingConsentType(); + if (currentConsentType == ConsentType.ConsentType_Individual) { + await zoom.declineRecordingConsent(); + Navigator.pop(context); + } else { + await zoom.declineRecordingConsent(); + zoom.leaveSession(false); + if (!context.mounted) return; + Navigator.popAndPushNamed( + context, + "Join", + arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + ); + } + }, + child: const Text('decline'), + ), + ], + ), + ); + } + isRecordingStarted.value = true; + } else { + isRecordingStarted.value = false; + } + }); + + final liveTranscriptionMsgInfoReceivedListener = eventListener.addListener(EventType.onLiveTranscriptionMsgInfoReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onLiveTranscriptionMsgInfoReceived: content: ${messageInfo.messageContent}"); + }); + + final inviteByPhoneStatusListener = eventListener.addListener(EventType.onInviteByPhoneStatus, (data) async { + data = data as Map; + debugPrint("onInviteByPhoneStatus: status: ${data['status']}, reason: ${data['reason']}"); + }); + + final multiCameraStreamStatusChangedListener = eventListener.addListener(EventType.onMultiCameraStreamStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var status = data['status']; + for (var user in users.value) { + { + if (changedUser.userId == user.userId) { + if (status == MultiCameraStreamStatus.Joined) { + user.hasMultiCamera = true; + } else if (status == MultiCameraStreamStatus.Left) { + user.hasMultiCamera = false; + } + } + } + } + }); + + final requireSystemPermission = eventListener.addListener(EventType.onRequireSystemPermission, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var permissionType = data['permissionType']; + switch (permissionType) { + case SystemPermissionType.Camera: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Camera"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + case SystemPermissionType.Microphone: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Microphone"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + } + }); + + final networkStatusChangeListener = eventListener.addListener(EventType.onUserVideoNetworkStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? networkUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + + if (data['status'] == NetworkStatus.Bad) { + debugPrint("onUserVideoNetworkStatusChanged: status: ${data['status']}, user: ${networkUser.userName}"); + } + }); + + final eventErrorListener = eventListener.addListener(EventType.onError, (data) async { + data = data as Map; + String errorType = data['errorType']; + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: Text(errorType), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + if (errorType == Errors.SessionJoinFailed || errorType == Errors.SessionDisconnecting) { + Timer( + const Duration(milliseconds: 1000), + () => { + Navigator.popAndPushNamed( + context, + "Join", + arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + ), + }); + } + }); + + final userRecordingConsentListener = eventListener.addListener(EventType.onUserRecordingConsent, (data) async { + data = data as Map; + ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + debugPrint('userRecordingConsentListener: user= ${user.userName}'); + }); + + final callCRCDeviceStatusListener = eventListener.addListener(EventType.onCallCRCDeviceStatusChanged, (data) async { + data = data as Map; + debugPrint('onCallCRCDeviceStatusChanged: status = ${data['status']}'); + }); + + final originalLanguageMsgReceivedListener = eventListener.addListener(EventType.onOriginalLanguageMsgReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onOriginalLanguageMsgReceived: content: ${messageInfo.messageContent}"); + }); + + final chatPrivilegeChangedListener = eventListener.addListener(EventType.onChatPrivilegeChanged, (data) async { + data = data as Map; + String type = data['privilege']; + debugPrint('chatPrivilegeChangedListener: type= $type'); + }); + + final testMicStatusListener = eventListener.addListener(EventType.onTestMicStatusChanged, (data) async { + data = data as Map; + String status = data['status']; + debugPrint('testMicStatusListener: status= $status'); + }); + + final micSpeakerVolumeChangedListener = eventListener.addListener(EventType.onMicSpeakerVolumeChanged, (data) async { + data = data as Map; + int type = data['micVolume']; + debugPrint('onMicSpeakerVolumeChanged: micVolume= $type, speakerVolume'); + }); + + final cameraControlRequestResultListener = eventListener.addListener(EventType.onCameraControlRequestResult, (data) async { + data = data as Map; + bool approved = data['approved']; + debugPrint('onCameraControlRequestResult: approved= $approved'); + }); + + final callOutUserJoinListener = eventListener.addListener(EventType.onCalloutJoinSuccess, (data) async { + data = data as Map; + String phoneNumber = data['phoneNumber']; + ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + debugPrint('onCalloutJoinSuccess: phoneNumber= $phoneNumber, user= ${user.userName}'); + }); + + return () => { + sessionJoinListener.cancel(), + sessionLeaveListener.cancel(), + sessionPasswordWrongListener.cancel(), + sessionNeedPasswordListener.cancel(), + userVideoStatusChangedListener.cancel(), + userAudioStatusChangedListener.cancel(), + userJoinListener.cancel(), + userLeaveListener.cancel(), + userNameChangedListener.cancel(), + userShareStatusChangeListener.cancel(), + liveStreamStatusChangeListener.cancel(), + cloudRecordingStatusListener.cancel(), + inviteByPhoneStatusListener.cancel(), + eventErrorListener.cancel(), + commandReceived.cancel(), + liveTranscriptionStatusChangeListener.cancel(), + liveTranscriptionMsgInfoReceivedListener.cancel(), + multiCameraStreamStatusChangedListener.cancel(), + requireSystemPermission.cancel(), + userRecordingConsentListener.cancel(), + networkStatusChangeListener.cancel(), + callCRCDeviceStatusListener.cancel(), + originalLanguageMsgReceivedListener.cancel(), + chatPrivilegeChangedListener.cancel(), + testMicStatusListener.cancel(), + micSpeakerVolumeChangedListener.cancel(), + cameraControlRequestResultListener.cancel(), + callOutUserJoinListener.cancel(), + shareContentChangedListener.cancel(), + }; + }, [zoom, users.value, isMounted]); + + void selectVirtualBackgroundItem() async { + final ImagePicker picker = ImagePicker(); + // Pick an image. + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + await zoom.virtualBackgroundHelper.addVirtualBackgroundItem(image!.path); + } + + void onPressAudio() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final audioStatus = mySelf.audioStatus; + if (audioStatus != null) { + var muted = await audioStatus.isMuted(); + if (muted) { + await zoom.audioHelper.unMuteAudio(mySelf.userId); + } else { + await zoom.audioHelper.muteAudio(mySelf.userId); + } + } + } + } + + void onPressVideo() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final videoStatus = mySelf.videoStatus; + if (videoStatus != null) { + var videoOn = await videoStatus.isOn(); + if (videoOn) { + await zoom.videoHelper.stopVideo(); + } else { + await zoom.videoHelper.startVideo(); + } + } + } + } + + void onPressShare() async { + var isOtherSharing = await zoom.shareHelper.isOtherSharing(); + var isShareLocked = await zoom.shareHelper.isShareLocked(); + String? shareCameraViewResult = Errors.InternalError; + + if (isOtherSharing) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Other is sharing'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isShareLocked) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Share is locked by host'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isSharing.value) { + zoom.shareHelper.stopShare(); + } else { + List options = [ + ListTile( + title: Text( + 'Share Device Screen', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + zoom.shareHelper.shareScreen(), + Navigator.of(context).pop(), + }, + ), + ListTile( + title: Text( + 'Share Camera', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + shareCameraViewResult = await zoom.shareHelper.startShareCamera(cameraShareView), + debugPrint('start camera: $shareCameraViewResult'), + Navigator.of(context).pop(), + }, + ), + ]; + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 60, + child: Scrollbar( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ), + ), + ); + }); + } + } + + void onSelectedUserVolume(ZoomVideoSdkUser user) async { + var isShareAudio = user.isSharing; + bool canSetVolume = await user.canSetUserVolume(user.userId, isShareAudio); + num userVolume; + + List options = [ + ListTile( + title: Text( + 'Adjust Volume', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ListTile( + title: Text( + 'Current volume', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + debugPrint('user volume'), + userVolume = await user.getUserVolume(user.userId, isShareAudio), + debugPrint('user ${user.userName}\'s volume is ${userVolume!}'), + }, + ), + ]; + if (canSetVolume) { + options.add( + ListTile( + title: Text( + 'Volume up', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume < 10) + { + await user.setUserVolume(user.userId, userVolume + 1, isShareAudio), + } + else + { + debugPrint("Cannot volume up."), + } + }, + ), + ); + options.add( + ListTile( + title: Text( + 'Volume down', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume > 0) + { + await user.setUserVolume(user.userId, userVolume - 1, isShareAudio), + } + else + { + debugPrint("Cannot volume down."), + } + }, + ), + ); + } + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 58, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ], + ), + )); + }); + } + + void onPressCameraList() async { + List options = []; + List cameraList = await zoom.videoHelper.getCameraList(); + for (var camera in cameraList) { + options.add( + ListTile( + title: Text( + camera.deviceName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + await zoom.videoHelper.switchCamera(camera.deviceId), + Navigator.of(context).pop(), + }, + ), + ); + } + options.add( + ListTile( + title: Text( + "Cancel", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + Navigator.of(context).pop(), + }, + ), + ); + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 60, + child: Scrollbar( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ), + ), + ); + }); + } + + // Future onPressMore() async { + // ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + // bool isShareLocked = await zoom.shareHelper.isShareLocked(); + // bool canSwitchSpeaker = await zoom.audioHelper.canSwitchSpeaker(); + // bool canStartRecording = (await zoom.recordingHelper.canStartRecording()) == Errors.Success; + // debugPrint("canStartRecording ${await zoom.recordingHelper.canStartRecording()}"); + // var startLiveTranscription = (await zoom.liveTranscriptionHelper.getLiveTranscriptionStatus()) == LiveTranscriptionStatus.Start; + // bool canStartLiveTranscription = await zoom.liveTranscriptionHelper.canStartLiveTranscription(); + // bool isHost = (mySelf != null) ? (await mySelf.getIsHost()) : false; + // isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(); + // bool canCallOutToCRC = await zoom.CRCHelper.isCRCEnabled(); + // bool supportVB = await zoom.virtualBackgroundHelper.isSupportVirtualBackground(); + // List? shareActionList = await mySelf?.getShareActionList(); + // + // List options = [ + // ListTile( + // title: Text( + // 'More', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.w600, + // color: Colors.black, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Get Chat Privilege', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("Chat Privilege = ${await zoom.chatHelper.getChatPrivilege()}"), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // 'Get Session Dial-in Number infos', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("session number = ${await zoom.session.getSessionNumber()}"), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // 'Get Camera List', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("camera list= ${await zoom.videoHelper.getCameraList()}"), + // onPressCameraList(), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // '${isMicOriginalOn.value ? 'Disable' : 'Enable'} Original Sound', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("${isMicOriginalOn.value}"), + // await zoom.audioSettingHelper.enableMicOriginalInput(!isMicOriginalOn.value), + // isMicOriginalOn.value = await zoom.audioSettingHelper.isMicOriginalInputEnable(), + // debugPrint("Original sound ${isMicOriginalOn.value ? 'Enabled' : 'Disabled'}"), + // Navigator.of(context).pop(), + // }, + // ) + // ]; + // + // if (supportVB) { + // options.add( + // ListTile( + // title: Text( + // 'Add Virtual Background', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // selectVirtualBackgroundItem(), + // Navigator.of(context).pop(), + // }, + // ), + // ); + // } + // + // if (canCallOutToCRC) { + // options.add(ListTile( + // title: Text( + // 'Call-out to CRC devices', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint('CRC result = ${await zoom.CRCHelper.callCRCDevice("bjn.vc", ZoomVideoSdkCRCProtocolType.SIP)}'), + // Navigator.of(context).pop(), + // }, + // )); + // options.add(ListTile( + // title: Text( + // 'Cancel call-out to CRC devices', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint('cancel result= ${await zoom.CRCHelper.cancelCallCRCDevice()}'), + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (canSwitchSpeaker) { + // options.add(ListTile( + // title: Text( + // 'Turn ${isSpeakerOn.value ? 'off' : 'on'} Speaker', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.audioHelper.setSpeaker(!isSpeakerOn.value), + // isSpeakerOn.value = await zoom.audioHelper.getSpeakerStatus(), + // debugPrint('Turned ${isSpeakerOn.value ? 'on' : 'off'} Speaker'), + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (isHost) { + // options.add(ListTile( + // title: Text( + // '${isShareLocked ? 'Unlock' : 'Lock'} Share', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("isShareLocked = ${await zoom.shareHelper.lockShare(!isShareLocked)}"), + // Navigator.of(context).pop(), + // })); + // options.add(ListTile( + // title: Text( + // 'Change Name', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () => { + // isRenameModalVisible.value = true, + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (canStartLiveTranscription) { + // options.add(ListTile( + // title: Text( + // "${startLiveTranscription ? 'Stop' : 'Start'} Live Transcription", + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (startLiveTranscription) + // { + // debugPrint('stopLiveTranscription= ${await zoom.liveTranscriptionHelper.stopLiveTranscription()}'), + // } + // else + // { + // debugPrint('startLiveTranscription= ${await zoom.liveTranscriptionHelper.startLiveTranscription()}'), + // }, + // Navigator.of(context).pop(), + // }, + // )); + // options.add(ListTile( + // title: Text( + // '${isReceiveSpokenLanguageContentEnabled.value ? 'Disable' : 'Enable'} receiving original caption', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.liveTranscriptionHelper.enableReceiveSpokenLanguageContent(!isReceiveSpokenLanguageContentEnabled.value), + // isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(), + // debugPrint("isReceiveSpokenLanguageContentEnabled = ${isReceiveSpokenLanguageContentEnabled.value}"), + // Navigator.of(context).pop(), + // })); + // } + // + // if (canStartRecording) { + // options.add(ListTile( + // title: Text( + // '${isRecordingStarted.value ? 'Stop' : 'Start'} Recording', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (!isRecordingStarted.value) + // { + // debugPrint('isRecordingStarted = ${await zoom.recordingHelper.startCloudRecording()}'), + // } + // else + // { + // debugPrint('isRecordingStarted = ${await zoom.recordingHelper.stopCloudRecording()}'), + // }, + // Navigator.of(context).pop(), + // })); + // } + // + // if (Platform.isAndroid) { + // bool isFlashlightSupported = await zoom.videoHelper.isSupportFlashlight(); + // bool isFlashlightOn = await zoom.videoHelper.isFlashlightOn(); + // if (isFlashlightSupported) { + // options.add(ListTile( + // title: Text( + // '${isFlashlightOn ? 'Turn Off' : 'Turn On'} Flashlight', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (!isFlashlightOn) + // { + // await zoom.videoHelper.turnOnOrOffFlashlight(true), + // } + // else + // { + // await zoom.videoHelper.turnOnOrOffFlashlight(false), + // }, + // Navigator.of(context).pop(), + // })); + // } + // } + // + // if (Platform.isIOS) { + // options.add(ListTile( + // title: Text( + // '${isPiPView.value ? 'Disable' : 'Enable'} picture in picture view', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // isPiPView.value = !isPiPView.value, + // Navigator.of(context).pop(), + // })); + // } + // + // if (isVideoOn.value) { + // options.add(ListTile( + // title: Text( + // 'Mirror the video', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.videoHelper.mirrorMyVideo(!isVideoMirrored.value), + // isVideoMirrored.value = await zoom.videoHelper.isMyVideoMirrored(), + // Navigator.of(context).pop(), + // })); + // options.add(ListTile( + // title: Text( + // '${isOriginalAspectRatio.value ? 'Enable' : 'Disable'} original aspect ratio', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.videoHelper.enableOriginalAspectRatio(!isOriginalAspectRatio.value), + // isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(), + // debugPrint("isOriginalAspectRatio= ${isOriginalAspectRatio.value}"), + // Navigator.of(context).pop(), + // })); + // } + // + // showDialog( + // context: context, + // builder: (context) { + // return Dialog( + // elevation: 0.0, + // insetPadding: const EdgeInsets.symmetric(horizontal: 40), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + // child: SizedBox( + // height: 500, + // child: Scrollbar( + // child: ListView( + // shrinkWrap: true, + // scrollDirection: Axis.vertical, + // children: ListTile.divideTiles( + // context: context, + // tiles: options, + // ).toList(), + // ), + // ), + // ), + // ); + // }); + // } + + void onLeaveSession(bool isEndSession) async { + await zoom.leaveSession(isEndSession); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + } + + void showLeaveOptions() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + bool isHost = await mySelf!.getIsHost(); + + Widget endSession; + Widget leaveSession; + Widget cancel = TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); //close Dialog + }, + ); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + endSession = TextButton( + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = TextButton( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + default: + endSession = CupertinoActionSheetAction( + isDestructiveAction: true, + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = CupertinoActionSheetAction( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + } + + List options = [ + leaveSession, + cancel, + ]; + + if (Platform.isAndroid) { + if (isHost) { + options.removeAt(1); + options.insert(0, endSession); + } + showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: const Text("Do you want to leave this session?"), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + actions: options, + ); + }); + } else { + options.removeAt(1); + if (isHost) { + options.insert(1, endSession); + } + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + message: const Text('Are you sure that you want to leave the session?'), + actions: options, + cancelButton: cancel, + ), + ); + } + } + + final chatMessageController = TextEditingController(); + + void sendChatMessage(String message) async { + await zoom.chatHelper.sendChatToAll(message); + ZoomVideoSdkUser? self = await zoom.session.getMySelf(); + ZoomVideoSdkCmdChannel cmdChannel = zoom.cmdChannel; + for (var user in users.value) { + if (user.userId != self?.userId) { + await zoom.cmdChannel.sendCommand(user.userId, message); + } + } + chatMessageController.clear(); + // send the chat as a command + } + + void onSelectedUser(ZoomVideoSdkUser user) async { + setState(() { + fullScreenUser.value = user; + }); + } + + changeNamePopup = Center( + child: Stack( + children: [ + Visibility( + visible: isRenameModalVisible.value, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.bottomLeft, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Colors.white, + ), + width: MediaQuery.of(context).size.width - 130, + height: MediaQuery.of(context).size.height * 0.2, + child: Center( + child: (Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, left: 20), + child: Text( + 'Change Name', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10, left: 20), + child: SizedBox( + width: MediaQuery.of(context).size.width - 230, + child: TextField( + onEditingComplete: () {}, + autofocus: true, + cursorColor: Colors.black, + controller: changeNameController, + decoration: InputDecoration( + isDense: true, + hintText: 'New name', + hintStyle: TextStyle( + fontSize: 14.0, + color: chatTextColor, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Apply', + style: const TextStyle( + fontSize: 16, + ), + ), + onTap: () async { + if (fullScreenUser.value != null) { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + await zoom.userHelper.changeName((mySelf?.userId)!, changeNameController.text); + changeNameController.clear(); + } + isRenameModalVisible.value = false; + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Cancel', + style: const TextStyle( + fontSize: 16, + ), + ), + onTap: () async { + isRenameModalVisible.value = false; + }, + ), + ) + ], + ), + ) + ], + )), + ), + ), + ], + )), + ], + ), + ); + + Widget fullScreenView; + Widget smallView; + Widget cameraView; + + if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) { + fullScreenView = AnimatedOpacity( + opacity: opacityLevel, + duration: const Duration(seconds: 3), + child: VideoView( + user: users.value.length > 1 ? users.value[1] : users.value[0], + hasMultiCamera: false, + isPiPView: isPiPView.value, + sharing: sharingUser.value == null ? false : (sharingUser.value?.userId == fullScreenUser.value?.userId), + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: true, + resolution: VideoResolution.Resolution360, + ), + ); + + smallView = Container( + height: 110, + margin: const EdgeInsets.only(left: 20, right: 20), + alignment: Alignment.center, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: users.value.length, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () async { + onSelectedUser(users.value[index]); + }, + onDoubleTap: () async { + onSelectedUserVolume(users.value[index]); + }, + child: Center( + child: VideoView( + user: users.value[index], + hasMultiCamera: false, + isPiPView: false, + sharing: false, + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: false, + resolution: VideoResolution.Resolution180, + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(), + ), + ); + } else { + fullScreenView = Container( + color: Colors.black, + child: const Center( + child: Text( + "Connecting...", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + )); + smallView = Container( + height: 110, + color: Colors.transparent, + ); + } + + cameraView = Offstage( + offstage: !isSharedCamera.value, + child: AnimatedOpacity( + opacity: isSharedCamera.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: cameraShareView, + ), + ); + + _changeOpacity; + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: backgroundColor, + body: Stack( + children: [ + // isScreenBeingShared ? fullScreenView : SizedBox.shrink(), + isScreenBeingShared ? SizedBox.shrink() : fullScreenView, + cameraView, + Container( + padding: const EdgeInsets.only(top: 35), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 80, + width: 180, + margin: const EdgeInsets.only(top: 16, left: 8), + padding: const EdgeInsets.all(8), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + // color: buttonBackgroundColor, + ), + // child: InkWell( + // onTap: () async { + // showDialog( + // context: context, + // builder: (context) { + // return Dialog( + // elevation: 0.0, + // insetPadding: const EdgeInsets.symmetric(horizontal: 40), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + // child: FractionallySizedBox( + // heightFactor: 0.2, + // widthFactor: 0.3, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // ListView( + // shrinkWrap: true, + // children: ListTile.divideTiles( + // context: context, + // tiles: [ + // ListTile( + // title: Text( + // 'Session Information', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.w600, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Session Name', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // sessionName.value, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Session Password', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // sessionPassword.value, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Participants', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // '${users.value.length}', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ], + // ).toList(), + // ), + // ], + // ), + // )); + // }); + // }, + // child: Stack( + // children: [ + // Column( + // children: [ + // const Padding(padding: EdgeInsets.symmetric(vertical: 4)), + // Align( + // alignment: Alignment.centerLeft, + // child: Text( + // sessionName.value, + // overflow: TextOverflow.ellipsis, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w600, + // color: Colors.white, + // ), + // ), + // ), + // ), + // const Padding(padding: EdgeInsets.symmetric(vertical: 5)), + // Align( + // alignment: Alignment.centerLeft, + // child: Text( + // "Participants: ${users.value.length}", + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w600, + // color: Colors.white, + // ), + // ), + // ), + // ) + // ], + // ), + // Container( + // alignment: Alignment.centerRight, + // child: Image.asset( + // "assets/icons/unlocked@2x.png", + // height: 22, + // )), + // ], + // ), + // ), + ), + TextButton( + onPressed: (showLeaveOptions), + child: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.only(top: 16, right: 8), + padding: const EdgeInsets.only(top: 5, bottom: 5, left: 16, right: 16), + height: 28, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + color: buttonBackgroundColor, + ), + child: const Text( + "LEAVE", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFFE02828), + ), + ), + )), + ], + ), + Align( + alignment: Alignment.centerRight, + child: FractionallySizedBox( + widthFactor: 0.2, + heightFactor: 0.6, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: onPressAudio, + icon: isMuted.value ? Image.asset("assets/images/png/zoom/unmute@2x.png") : Image.asset("assets/images/png/zoom/mute@2x.png"), + iconSize: circleButtonSize, + tooltip: isMuted.value == true ? "Unmute" : "Mute", + ), + IconButton( + onPressed: onPressShare, + icon: isSharing.value ? Image.asset("assets/images/png/zoom/share-off@2x.png") : Image.asset("assets/images/png/zoom/share-on@2x.png"), + iconSize: circleButtonSize, + ), + IconButton( + onPressed: onPressVideo, + iconSize: circleButtonSize, + icon: isVideoOn.value ? Image.asset("assets/images/png/zoom/video-off@2x.png") : Image.asset("assets/images/png/zoom/video-on@2x.png"), + ), + Column( + children: [ + IconButton( + onPressed: () async { + onPressCameraList(); + }, + icon: Icon(Icons.cameraswitch, color: Colors.white), + color: Colors.white, + iconSize: 40.h, + ), + "Switch Camera".toText12(color: AppColors.whiteColor, isCenter: true, isBold: true) + ], + ), + ], + ), + )), + Container( + margin: const EdgeInsets.only(left: 16, right: 16, bottom: 40, top: 10), + alignment: Alignment.bottomCenter, + child: SizedBox( + height: MediaQuery.of(context).viewInsets.bottom == 0 ? 65 : MediaQuery.of(context).viewInsets.bottom + 18, + child: TextField( + maxLines: 1, + textAlign: TextAlign.left, + style: TextStyle(color: chatTextColor), + cursorColor: chatTextColor, + textAlignVertical: TextAlignVertical.center, + controller: chatMessageController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(left: 16, top: 10, bottom: 10, right: 16), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(width: 1, color: chatTextColor), //<-- SEE HERE + ), + hintText: 'Type your message here...', + hintStyle: TextStyle( + fontSize: 14.0, + color: chatTextColor, + ), + ), + onSubmitted: (String str) { + sendChatMessage(str); + }, + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(bottom: 120), + child: smallView, + ), + // CommentList(zoom: zoom, eventListener: eventListener), + changeNamePopup, + ], + )), + ], + )); + } +} + +class CallArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionIdleTimeoutMins; + final String role; + final int callType; + + CallArguments(this.sessionName, this.sessionPwd, this.displayName, this.sessionIdleTimeoutMins, this.role, this.isJoin, this.callType); +} + +class JoinArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionTimeout; + final String roleType; + + JoinArguments(this.isJoin, this.sessionName, this.sessionPwd, this.displayName, this.sessionTimeout, this.roleType); +} diff --git a/lib/presentation/tele_consultation/zoom/video_view.dart b/lib/presentation/tele_consultation/zoom/video_view.dart new file mode 100644 index 0000000..4c1e049 --- /dev/null +++ b/lib/presentation/tele_consultation/zoom/video_view.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as FlutterZoomView; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; + +const SHOW_TALKING_ICON_DURATION = 2000; + +class VideoView extends FlutterZoomView.ZoomView { + const VideoView({ + super.key, + required super.user, + required super.sharing, + required super.preview, + required super.focused, + required super.hasMultiCamera, + required super.multiCameraIndex, + required super.videoAspect, + required super.fullScreen, + required super.resolution, + required super.isPiPView, + }); + + @override + Widget build(BuildContext context) { + var isVideoOn = useState(false); + var isTalking = useState(false); + var isMuted = useState(false); + var isMounted = useIsMounted(); + var zoom = ZoomVideoSdk(); + var isSharing = useState(false); + user?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + + useEffect(() { + updateVideoStatus() { + if (user == null) return; + Future.microtask(() async { + if (isMounted()) { + isVideoOn.value = (await user!.videoStatus!.isOn()); + isSharing.value = sharing; + } + }); + } + + resetAudioStatus() { + isTalking.value = false; + isMuted.value = false; + } + + updateAudioStatus() async { + if (!isMounted()) return; + var talking = await user?.audioStatus?.isTalking(); + var muted = await user?.audioStatus?.isMuted(); + isMuted.value = muted!; + isTalking.value = talking!; + if (talking) { + Timer( + const Duration(milliseconds: SHOW_TALKING_ICON_DURATION), + () => { + if (isMounted()) + { + isTalking.value = false, + } + }); + } + } + + updateVideoStatus(); + return null; + }, [zoom, user]); + + ImageIcon audioStatusIcon; + if (isTalking.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/images/png/zoom/talking@2x.png"), + ); + } else if (isMuted.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/images/png/zoom/muted@2x.png"), + ); + } + // Pass parameters to the platform side. + final Map creationParams = {}; + creationParams.putIfAbsent("userId", () => user?.userId); + creationParams.putIfAbsent("sharing", () => sharing); + creationParams.putIfAbsent("preview", () => preview); + creationParams.putIfAbsent("focused", () => focused); + creationParams.putIfAbsent("hasMultiCamera", () => hasMultiCamera); + if (videoAspect.isEmpty) { + creationParams.putIfAbsent("videoAspect", () => VideoAspect.PanAndScan); + } else { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + creationParams.putIfAbsent("fullScreen", () => fullScreen); + if (resolution.isNotEmpty) { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + + if (fullScreen) { + if (sharing) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else if (isVideoOn.value) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else { + return Container( + margin: const EdgeInsets.symmetric(vertical: 0), + child: Container( + alignment: Alignment.center, + child: const Image( + image: AssetImage("assets/images/png/zoom/default-avatar.png"), + )), + ); + } + } else { + if (isVideoOn.value || sharing) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ), + Container( + height: 110, + width: 110, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + Container( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + // height: 110, + // width: 110, + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/images/png/zoom/muted@2x.png") : const AssetImage("assets/images/png/zoom/talking@2x.png"), + fit: BoxFit.cover, + ), + ) + ], + ), + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: Container( + alignment: Alignment.center, + child: const Image( + height: 60, + width: 60, + image: AssetImage("assets/images/png/zoom/default-avatar.png"), + )), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/images/png/zoom/muted@2x.png") : const AssetImage("assets/images/png/zoom/talking@2x.png"), + fit: BoxFit.cover, + ), + ), + ], + ), + ); + } + } + } +} diff --git a/lib/presentation/todo/todo_page.dart b/lib/presentation/todo/todo_page.dart deleted file mode 100644 index 65b762f..0000000 --- a/lib/presentation/todo/todo_page.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; - -class ToDoPage extends StatelessWidget { - const ToDoPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - appBar: AppBar( - title: const Text('Appointments'), - backgroundColor: AppColors.bgScaffoldColor, - ), - body: const Center( - child: Text( - 'Appointments Page', - style: TextStyle(fontSize: 24), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart new file mode 100644 index 0000000..fdfcc04 --- /dev/null +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -0,0 +1,643 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class AncillaryOrderPaymentPage extends StatefulWidget { + final DateTime? appointmentDate; + final int appointmentNoVida; + final int orderNo; + final int projectID; + final List selectedProcedures; + final double totalAmount; + + const AncillaryOrderPaymentPage({ + super.key, + required this.appointmentDate, + required this.appointmentNoVida, + required this.orderNo, + required this.projectID, + required this.selectedProcedures, + required this.totalAmount, + }); + + @override + State createState() => _AncillaryOrderPaymentPageState(); +} + +class _AncillaryOrderPaymentPageState extends State { + late PayfortViewModel payfortViewModel; + late AppState appState; + late TodoSectionViewModel todoSectionViewModel; + + MyInAppBrowser? browser; + String selectedPaymentMethod = ""; + String transID = ""; + + @override + void initState() { + scheduleMicrotask(() { + payfortViewModel.initPayfortViewModel(); + payfortViewModel.setIsApplePayConfigurationLoading(false); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + todoSectionViewModel = Provider.of(context); + payfortViewModel = Provider.of(context); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, todoVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Select Payment Method".needTranslation, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + + // Mada Payment Option + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: todoVM.isProcessingPayment), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true).toShimmer2(isShow: todoVM.isProcessingPayment), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ).toShimmer2(isShow: todoVM.isProcessingPayment), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (!todoVM.isProcessingPayment) { + selectedPaymentMethod = "MADA"; + _openPaymentURL("mada"); + } + }), + + SizedBox(height: 16.h), + + // Visa/Mastercard Payment Option + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), + ], + ).toShimmer2(isShow: todoVM.isProcessingPayment), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true).toShimmer2(isShow: todoVM.isProcessingPayment), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ).toShimmer2(isShow: todoVM.isProcessingPayment), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (!todoVM.isProcessingPayment) { + selectedPaymentMethod = "VISA"; + _openPaymentURL("visa"); + } + }), + ], + ), + ), + ), + ), + // Payment Summary Footer + todoVM.isProcessingPayment ? SizedBox.shrink() : _buildPaymentSummary() + ], + ); + }, + ), + ); + } + + Widget _buildPaymentSummary() { + // Calculate amounts + double amountBeforeTax = 0.0; + double taxAmount = 0.0; + + for (var proc in widget.selectedProcedures) { + amountBeforeTax += (proc.patientShare ?? 0); + taxAmount += (proc.patientTaxAmount ?? 0); + } + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Consumer(builder: (context, payfortVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + + // Amount before tax + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + amountBeforeTax.toStringAsFixed(2).toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + + // VAT 15% + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + taxAmount.toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), + AppColors.greyTextColor, + 13, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + + SizedBox(height: 17.h), + + // Total Amount + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + widget.totalAmount.toStringAsFixed(2).toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + + // Apple Pay Button (iOS only) + Platform.isIOS && Utils.havePrivilege(103) + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (!todoSectionViewModel.isProcessingPayment) { + _startApplePay(); + } + }) + : SizedBox(height: 12.h), + + SizedBox(height: 12.h), + ], + ); + }), + ); + } + + void _openPaymentURL(String paymentMethod) { + todoSectionViewModel.setProcessingPayment(true); + + browser = MyInAppBrowser( + onExitCallback: _onBrowserExit, + onLoadStartCallback: _onBrowserLoadStart, + ); + + final user = appState.getAuthenticatedUser(); + transID = Utils.getAdvancePaymentTransID( + widget.projectID, + user!.patientId!, + ); + + browser!.openPaymentBrowser( + widget.totalAmount, + "Ancillary Order Payment", + transID, + widget.projectID.toString(), + user.emailAddress ?? "CustID_${user.patientId}@HMG.com", + paymentMethod, + user.patientType ?? 1, + "${user.firstName} ${user.lastName}", + user.patientId, + user, + browser!, + false, + "3", + ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum().toString(), + context, + null, + widget.appointmentNoVida, + 0, + 0, + null, + ); + } + + void _onBrowserLoadStart(String url) { + log("onBrowserLoadStart: $url"); + + for (var element in MyInAppBrowser.successURLS) { + if (url.contains(element)) { + if (browser!.isOpened()) browser!.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + } + + for (var element in MyInAppBrowser.errorURLS) { + if (url.contains(element)) { + if (browser!.isOpened()) browser!.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + } + } + + void _onBrowserExit(bool isPaymentMade) { + log("onBrowserExit Called: $isPaymentMade"); + _checkPaymentStatus(); + } + + void _checkPaymentStatus() { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + + todoSectionViewModel.checkPaymentStatus( + transID: transID, + onSuccess: (response) { + String paymentInfo = response['Response_Message']; + + if (paymentInfo == 'Success') { + // Extract payment details from response + final paymentAmount = response['Amount'] ?? widget.totalAmount; + final fortId = response['Fort_id'] ?? transID; + final paymentMethod = response['PaymentMethod'] ?? selectedPaymentMethod; + + // Call createAdvancePayment with the payment details + _createAdvancePayment( + paymentAmount: paymentAmount is String ? double.parse(paymentAmount) : paymentAmount.toDouble(), + paymentReference: fortId, + paymentMethod: paymentMethod, + ); + } else { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast(response['Response_Message']); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast(error); + }, + ); + } + + void _createAdvancePayment({ + required double paymentAmount, + required String paymentReference, + required String paymentMethod, + }) { + LoaderBottomSheet.showLoader(loadingText: "Processing payment, Please wait...".needTranslation); + + final user = appState.getAuthenticatedUser(); + + todoSectionViewModel.createAdvancePayment( + projectID: widget.projectID, + paymentAmount: paymentAmount, + paymentReference: paymentReference, + paymentMethodName: paymentMethod, + patientTypeID: user!.patientType ?? 1, + patientName: "${user.firstName} ${user.lastName}", + patientID: user.patientId!, + setupID: "010266", + isAncillaryOrder: true, + onSuccess: (response) { + // Extract advance number from response + final advanceNumber = + response['OnlineCheckInAppointments']?[0]?['AdvanceNumber'] ?? response['OnlineCheckInAppointments']?[0]?['AdvanceNumber_VP'] ?? ''; + + if (advanceNumber.isNotEmpty) { + _addAdvancedNumberRequest( + advanceNumber: advanceNumber.toString(), + paymentReference: paymentReference, + ); + } else { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast("Failed to get advance number"); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast(error); + }, + ); + } + + void _addAdvancedNumberRequest({ + required String advanceNumber, + required String paymentReference, + }) { + LoaderBottomSheet.showLoader(loadingText: "Finalizing payment, Please wait...".needTranslation); + + final user = appState.getAuthenticatedUser(); + + todoSectionViewModel.addAdvancedNumberRequest( + advanceNumber: advanceNumber, + paymentReference: paymentReference, + appointmentID: 0, + patientID: user!.patientId!, + patientTypeID: user.patientType ?? 1, + patientOutSA: user.outSa ?? 0, + onSuccess: (response) { + // After adding advance number, generate invoice + _autoGenerateInvoice(); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast(error); + }, + ); + } + + void _autoGenerateInvoice() { + LoaderBottomSheet.showLoader(loadingText: "Generating invoice, Please wait...".needTranslation); + + List selectedProcListAPI = widget.selectedProcedures.map((element) { + return { + "ApprovalLineItemNo": element.approvalLineItemNo, + "OrderLineItemNo": element.orderLineItemNo, + "ProcedureID": element.procedureID, + }; + }).toList(); + + todoSectionViewModel.autoGenerateAncillaryOrdersInvoice( + orderNo: widget.orderNo, + projectID: widget.projectID, + appointmentNo: widget.appointmentNoVida, + selectedProcedures: selectedProcListAPI, + languageID: appState.isArabic() ? 1 : 2, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + + final invoiceNo = response['AncillaryOrderInvoiceList']?[0]?['InvoiceNo']; + + _showSuccessDialog(invoiceNo); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + Utils.showToast(error); + }, + ); + } + + void _showSuccessDialog(dynamic invoiceNo) { + todoSectionViewModel.setProcessingPayment(false); + + log("Ancillary order payment successful! Invoice #: $invoiceNo"); + + // Show success message and navigate + Utils.showToast("Payment successful! Invoice #: $invoiceNo"); + // Navigate back to home after a short delay + Future.delayed(Duration(seconds: 1), () { + showCommonBottomSheetWithoutHeight( + context, + child: Column( + children: [ + Row( + children: [ + "Here is your invoice #: ".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + ("12345").toText16(isBold: true), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ], + ), + // title: "Payment Completed Successfully".needTranslation, + titleWidget: Utils.getSuccessWidget(loadingText: "Payment Completed Successfully".needTranslation), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + }); + } + + _startApplePay() async { + showCommonBottomSheet( + context, + child: Utils.getLoadingWidget(), + callBackFunc: (str) {}, + title: "", + height: ResponsiveExtension.screenHeight * 0.3, + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + final user = appState.getAuthenticatedUser(); + transID = Utils.getAdvancePaymentTransID(widget.projectID, user!.patientId!); + + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); + await payfortViewModel.getPayfortConfigurations( + serviceId: ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum(), + projectId: widget.projectID, + integrationId: 2, + ); + + applePayInsertRequest.clientRequestID = transID; + applePayInsertRequest.clinicID = 0; + + applePayInsertRequest.currency = appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com"; + applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + + applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken); + applePayInsertRequest.doctorID = 0; + applePayInsertRequest.projectID = widget.projectID.toString(); + applePayInsertRequest.serviceID = ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum().toString(); + applePayInsertRequest.channelID = 3; + applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType; + applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa; + applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(widget.appointmentDate ?? DateTime.now()); + applePayInsertRequest.appointmentNo = widget.appointmentNoVida; + applePayInsertRequest.orderDescription = "Ancillary Order Payment"; + applePayInsertRequest.liveServiceID = "0"; + applePayInsertRequest.latitude = "0.0"; + applePayInsertRequest.longitude = "0.0"; + applePayInsertRequest.amount = widget.totalAmount.toString(); + applePayInsertRequest.isSchedule = "0"; + applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en'; + applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2; + applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId; + applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.paymentOption = "ApplePay"; + + applePayInsertRequest.isMobSDK = true; + applePayInsertRequest.merchantReference = transID; + applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.commandType = "PURCHASE"; + applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.returnURL = ""; + + try { + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest); + } catch (error) { + log("Apple Pay Insert Request Failed: $error"); + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Failed to initialize Apple Pay. Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + return; + } + // Only proceed with Apple Pay if insert was successful + payfortViewModel.paymentWithApplePay( + customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + orderDescription: "Ancillary Order Payment", + orderAmount: widget.totalAmount, + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier, + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode, + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest, + currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + log("failureResult: ${failureResult.message.toString()}"); + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + Navigator.of(context).pop(); + log("successResult: ${successResult.responseMessage.toString()}"); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + _checkPaymentStatus(); + }, + ); + } +} diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart new file mode 100644 index 0000000..b7515af --- /dev/null +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -0,0 +1,719 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_order_payment_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class AncillaryOrderDetailsList extends StatefulWidget { + final int appointmentNoVida; + final int orderNo; + final int projectID; + final String projectName; + + const AncillaryOrderDetailsList({ + super.key, + required this.appointmentNoVida, + required this.orderNo, + required this.projectID, + required this.projectName, + }); + + @override + State createState() => _AncillaryOrderDetailsListState(); +} + +class _AncillaryOrderDetailsListState extends State { + late TodoSectionViewModel todoSectionViewModel; + late AppState appState; + List selectedProcedures = []; + + @override + void initState() { + super.initState(); + appState = getIt.get(); + todoSectionViewModel = context.read(); + scheduleMicrotask(() async { + await todoSectionViewModel.getPatientOnlineAncillaryOrderDetailsProceduresList( + appointmentNoVida: widget.appointmentNoVida, + orderNo: widget.orderNo, + projectID: widget.projectID, + onSuccess: (response) { + _autoSelectEligibleProcedures(); + }, + ); + }); + } + + void _autoSelectEligibleProcedures() { + selectedProcedures.clear(); + if (todoSectionViewModel.patientAncillaryOrderProceduresList.isNotEmpty) { + final procedures = todoSectionViewModel.patientAncillaryOrderProceduresList[0].ancillaryOrderProcDetailsList; + if (procedures != null) { + for (var proc in procedures) { + if (!_isProcedureDisabled(proc)) { + selectedProcedures.add(proc); + } + } + } + } + setState(() {}); + } + + bool _isProcedureDisabled(AncillaryOrderProcDetail procedure) { + // return true; + return (procedure.isApprovalRequired == true && procedure.isApprovalCreated == false) || + (procedure.isApprovalCreated == true && procedure.approvalNo == 0) || + (procedure.isApprovalRequired == true && procedure.isApprovalCreated == true && procedure.approvalNo == 0); + } + + bool _isProcedureSelected(AncillaryOrderProcDetail procedure) { + return selectedProcedures.contains(procedure); + } + + void _toggleProcedureSelection(AncillaryOrderProcDetail procedure) { + setState(() { + if (_isProcedureSelected(procedure)) { + selectedProcedures.remove(procedure); + } else { + selectedProcedures.add(procedure); + } + }); + } + + String _getApprovalStatusText(AncillaryOrderProcDetail procedure) { + if (procedure.isApprovalRequired == false) { + return "Cash"; + } else { + if (procedure.isApprovalCreated == true && procedure.approvalNo != 0) { + return "Approved"; + } else if (procedure.isApprovalRequired == true && procedure.isApprovalCreated == true && procedure.approvalNo == 0) { + return "Approval Rejected - Please visit receptionist"; + } else { + return "Sent For Approval"; + } + } + } + + double _getTotalAmount() { + double total = 0.0; + for (var proc in selectedProcedures) { + total += (proc.patientShareWithTax ?? 0); + } + return total; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, viewModel, child) { + AncillaryOrderProcedureItem? orderData; + if (viewModel.patientAncillaryOrderProceduresList.isNotEmpty) { + orderData = viewModel.patientAncillaryOrderProceduresList[0]; + } + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Ancillary Order Details".needTranslation, + child: viewModel.isAncillaryDetailsProceduresLoading + ? _buildLoadingShimmer().paddingSymmetrical(24.w, 0) + : viewModel.patientAncillaryOrderProceduresList.isEmpty + ? _buildDefaultEmptyState(context).paddingSymmetrical(24.w, 0) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + if (orderData != null) _buildPatientInfoCard(orderData), + SizedBox(height: 16.h), + if (orderData != null) _buildProceduresSection(orderData), + ], + ).paddingSymmetrical(24.w, 0), + ), + ), + ), + if (orderData != null) _buildStickyPaymentButton(orderData), + ], + ); + }), + ); + } + + Widget _buildLoadingShimmer() { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + itemBuilder: (context, index) { + return AncillaryOrderCard( + order: AncillaryOrderItem(), + isLoading: true, + ); + }, + ); + } + + Widget _buildDefaultEmptyState(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "No Procedures available for the selected order.".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } + + Widget _buildPatientInfoCard(AncillaryOrderProcedureItem orderData) { + final user = appState.getAuthenticatedUser(); + final patientName = orderData.patientName ?? user?.firstName ?? "N/A"; + final patientMRN = orderData.patientID ?? user?.patientId; + final nationalID = user?.patientIdentificationNo ?? ""; + + // Determine gender for profile image (assuming 1 = male, 2 = female) + final gender = user?.gender ?? 1; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row with Profile Image, Name, and QR Code + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, + width: 56.w, + height: 56.h, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + patientName.toText18( + isBold: true, + weight: FontWeight.w600, + textOverflow: TextOverflow.ellipsis, + maxlines: 2, + ), + ], + ), + ), + ], + ), + + SizedBox(height: 12.h), + + Wrap( + alignment: WrapAlignment.start, + spacing: 4.w, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + // icon: AppAssets.file_icon, + labelText: "MRN: ${patientMRN ?? 'N/A'}", + iconSize: 12.w, + ), + + // National ID + if (nationalID.isNotEmpty) + AppCustomChipWidget( + // icon: AppAssets.card_user, + labelText: "ID: $nationalID", + iconSize: 12.w, + ), + + // Appointment Number + if (orderData.appointmentNo != null) + AppCustomChipWidget( + // icon: AppAssets.calendar, + labelText: "Appt #: ${orderData.appointmentNo}", + iconSize: 12.w, + ), + + // Order Number + if (orderData.ancillaryOrderProcDetailsList?.firstOrNull?.orderNo != null) + AppCustomChipWidget( + labelText: "Order #: ${orderData.ancillaryOrderProcDetailsList!.first.orderNo}", + ), + + // Blood Group + if (user?.bloodGroup != null && user!.bloodGroup!.isNotEmpty) + AppCustomChipWidget( + // icon: AppAssets.blood_icon, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), + labelText: "Blood: ${user.bloodGroup}", + iconColor: AppColors.primaryRedColor, + ), + + // Insurance Company (if applicable) + if (orderData.companyName != null && orderData.companyName!.isNotEmpty) + AppCustomChipWidget( + icon: AppAssets.insurance_active_icon, + labelText: orderData.companyName!, + iconColor: AppColors.successColor, + backgroundColor: AppColors.successColor.withValues(alpha: 0.15), + iconSize: 12.w, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), + ), + + // Policy Number + if (orderData.insurancePolicyNo != null && orderData.insurancePolicyNo!.isNotEmpty) + AppCustomChipWidget( + labelText: "Policy: ${orderData.insurancePolicyNo}", + ), + + AppCustomChipWidget( + labelText: "Doctor: ${orderData.doctorName ?? "N/A"}", + ), + if (widget.projectName.isNotEmpty) + AppCustomChipWidget( + labelText: widget.projectName, + ), + if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty) + AppCustomChipWidget( + labelText: "Clinic: ${orderData.clinicName!}", + ), + if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty) + AppCustomChipWidget( + labelText: "Date: ${DateFormat('MMM dd, yyyy').format(orderData.appointmentDate!)}", + ), + ], + ), + + // SizedBox(height: 12.h), + // + // // Additional Details Section + // Container( + // padding: EdgeInsets.all(12.h), + // decoration: BoxDecoration( + // color: AppColors.bgScaffoldColor, + // borderRadius: BorderRadius.circular(12.r), + // ), + // child: Column( + // children: [ + // _buildInfoRow( + // "Doctor".needTranslation, + // orderData.doctorName ?? "N/A", + // ), + // if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty) + // _buildInfoRow( + // "Clinic".needTranslation, + // orderData.clinicName!, + // ), + // if (orderData.appointmentDate != null) + // _buildInfoRow( + // "Appointment Date".needTranslation, + // DateFormat('MMM dd, yyyy').format(orderData.appointmentDate!), + // ), + // ], + // ), + // ), + ], + ).paddingOnly(top: 16.h, right: 16.w, left: 16.w, bottom: 12.h), + + // Divider + Container(height: 1, color: AppColors.dividerColor), + + // Summary Section + ], + ), + ); + } + + Widget _buildSummarySection(AncillaryOrderProcedureItem orderData) { + final totalProcedures = orderData.ancillaryOrderProcDetailsList?.length ?? 0; + final selectedCount = selectedProcedures.length; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Procedures".needTranslation.toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w600, + ), + "$selectedCount of $totalProcedures selected".toText14( + isBold: true, + weight: FontWeight.bold, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + "Total Amount".needTranslation.toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w600, + ), + Row( + children: [ + Utils.getPaymentAmountWithSymbol( + _getTotalAmount().toStringAsFixed(2).toText14( + isBold: true, + weight: FontWeight.bold, + color: AppColors.primaryRedColor, + ), + AppColors.textColorLight, + 13, + isSaudiCurrency: true, + ), + // + // _getTotalAmount().toStringAsFixed(2).toText14( + // isBold: true, + // weight: FontWeight.bold, + // color: AppColors.primaryRedColor, + // ), + // SizedBox(width: 4.w), + // "SAR".toText12(color: AppColors.textColorLight), + ], + ), + ], + ), + ], + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: "$label:".toText12(color: AppColors.textColorLight, fontWeight: FontWeight.w600), + ), + SizedBox(width: 8.w), + Expanded( + flex: 3, + child: value.toText12(color: AppColors.textColor, fontWeight: FontWeight.w600), + ), + ], + ), + ); + } + + Widget _buildProceduresSection(AncillaryOrderProcedureItem orderData) { + if (orderData.ancillaryOrderProcDetailsList == null || orderData.ancillaryOrderProcDetailsList!.isEmpty) { + return SizedBox.shrink(); + } + + // Group procedures by category + final groupedProcedures = groupBy( + orderData.ancillaryOrderProcDetailsList!, + (AncillaryOrderProcDetail proc) => proc.procedureCategoryName ?? "Other", + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: groupedProcedures.entries.map((entry) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + entry.key.toText18(isBold: true), + SizedBox(height: 12.h), + ...entry.value.map((procedure) => _buildProcedureCard(procedure)), + SizedBox(height: 16.h), + ], + ); + }).toList(), + ); + } + + Widget _buildProcedureCard(AncillaryOrderProcDetail procedure) { + final isDisabled = _isProcedureDisabled(procedure); + // final isDisabled = _isProcedureDisabled(procedure); + final isSelected = _isProcedureSelected(procedure); + + return AnimationConfiguration.staggeredList( + position: 0, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.only(bottom: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: isDisabled ? AppColors.greyColor : AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: !isDisabled, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: isDisabled ? null : () => _toggleProcedureSelection(procedure), + borderRadius: BorderRadius.circular(24.h), + child: Container( + padding: EdgeInsets.all(14.h), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.h)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isDisabled) + Padding( + padding: EdgeInsets.only(right: 8.w), + child: Checkbox( + value: isSelected, + onChanged: (v) => _toggleProcedureSelection(procedure), + activeColor: AppColors.primaryRedColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (procedure.procedureName ?? "N/A").toText14(isBold: true, maxlines: 2), + ], + ), + ), + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: _getApprovalStatusText(procedure), + // backgroundColor: , + ), + // if (procedure.procedureID != null) + // AppCustomChipWidget( + // labelText: "ID: ${procedure.procedureID}", + // ), + if (procedure.isCovered == true) + AppCustomChipWidget( + labelText: "Covered".needTranslation, + backgroundColor: AppColors.successColor.withValues(alpha: 0.1), + textColor: AppColors.successColor, + ), + ], + ), + SizedBox(height: 12.h), + Container(height: 1, color: AppColors.dividerColor), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Price".needTranslation.toText10(color: AppColors.textColorLight), + SizedBox(height: 4.h), + Row( + children: [ + Utils.getPaymentAmountWithSymbol( + (procedure.patientShare ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + AppColors.textColorLight, + 13, + isSaudiCurrency: true, + ), + ], + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "VAT (15%)".needTranslation.toText10(color: AppColors.textColorLight), + SizedBox(height: 4.h), + Row( + children: [ + Utils.getPaymentAmountWithSymbol( + (procedure.patientTaxAmount ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + AppColors.textColorLight, + 13, + isSaudiCurrency: true, + ), + ], + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Total".needTranslation.toText10(color: AppColors.textColorLight), + SizedBox(height: 4.h), + Row( + children: [ + Utils.getPaymentAmountWithSymbol( + (procedure.patientShareWithTax ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + AppColors.textColorLight, + 13, + isSaudiCurrency: true, + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + )); + } + + Widget _buildStickyPaymentButton(orderData) { + final isButtonEnabled = selectedProcedures.isNotEmpty; + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor)), + ], + ), + SizedBox(height: 18.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150.h, + child: Utils.getPaymentMethods(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Utils.getPaymentAmountWithSymbol(_getTotalAmount().toStringAsFixed(2).toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), + ], + ), + ], + ), + SizedBox(height: 16.h), + CustomButton( + borderWidth: 0, + backgroundColor: AppColors.infoLightColor, + text: "Proceed to Payment".needTranslation, + onPressed: () { + // Navigate to payment page with selected procedures + Navigator.of(context).push( + CustomPageRoute( + page: AncillaryOrderPaymentPage( + appointmentNoVida: widget.appointmentNoVida, + orderNo: widget.orderNo, + projectID: widget.projectID, + selectedProcedures: selectedProcedures, + totalAmount: _getTotalAmount(), + appointmentDate: orderData.appointmentDate, + ), + ), + ); + }, + isDisabled: !isButtonEnabled, + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + padding: EdgeInsets.symmetric(vertical: 16.h), + ), + SizedBox(height: 22.h), + ], + ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), + ); + + // Column( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // SizedBox(height: 16.h), + // _buildSummarySection(orderData), + // SizedBox(height: 16.h), + // CustomButton( + // borderWidth: 0, + // backgroundColor: AppColors.infoLightColor, + // text: "Proceed to Payment".needTranslation, + // onPressed: () { + // // Navigate to payment page with selected procedures + // Navigator.of(context).push( + // CustomPageRoute( + // page: AncillaryOrderPaymentPage( + // appointmentNoVida: widget.appointmentNoVida, + // orderNo: widget.orderNo, + // projectID: widget.projectID, + // selectedProcedures: selectedProcedures, + // totalAmount: _getTotalAmount(), + // appointmentDate: orderData.appointmentDate, + // ), + // ), + // ); + // }, + // isDisabled: !isButtonEnabled, + // textColor: AppColors.whiteColor, + // borderRadius: 12.r, + // borderColor: Colors.transparent, + // padding: EdgeInsets.symmetric(vertical: 16.h), + // ), + // SizedBox(height: 22.h), + // ], + // ).paddingSymmetrical(24.w, 0); + } +} diff --git a/lib/presentation/todo_section/todo_page.dart b/lib/presentation/todo_section/todo_page.dart new file mode 100644 index 0000000..0d2d806 --- /dev/null +++ b/lib/presentation/todo_section/todo_page.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class ToDoPage extends StatefulWidget { + const ToDoPage({super.key}); + + @override + State createState() => _ToDoPageState(); +} + +class _ToDoPageState extends State { + + late AppState appState; + + @override + void initState() { + final TodoSectionViewModel todoSectionViewModel = context.read(); + scheduleMicrotask(() async { + if (appState.isAuthenticated) { + await todoSectionViewModel.initializeTodoSectionViewModel(); + } + }); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Widget _buildLoadingShimmer() { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + return AncillaryOrderCard( + order: AncillaryOrderItem(), + isLoading: true, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return CollapsingListView( + title: "Ancillary Orders".needTranslation, + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + Consumer( + builder: (BuildContext context, TodoSectionViewModel todoSectionViewModel, Widget? child) { + return todoSectionViewModel.isAncillaryOrdersLoading + ? _buildLoadingShimmer() + : AncillaryOrdersList( + orders: todoSectionViewModel.patientAncillaryOrdersList, + onCheckIn: (order) => log("Check-in for order: ${order.orderNo}"), + onViewDetails: (order) async { + Navigator.of(context).push(CustomPageRoute( + page: AncillaryOrderDetailsList( + appointmentNoVida: order.appointmentNo ?? 0, + orderNo: order.orderNo ?? 0, + projectID: order.projectID ?? 0, + projectName: order.projectName ?? "", + ), + ), + ); + }, + ); + }, + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart new file mode 100644 index 0000000..78d7e3e --- /dev/null +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -0,0 +1,278 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +class AncillaryOrdersList extends StatelessWidget { + final List orders; + final Function(AncillaryOrderItem order)? onCheckIn; + final Function(AncillaryOrderItem order)? onViewDetails; + + const AncillaryOrdersList({ + super.key, + required this.orders, + this.onCheckIn, + this.onViewDetails, + }); + + @override + Widget build(BuildContext context) { + // Show empty state + if (orders.isEmpty) { + return _buildDefaultEmptyState(context); + } + + // Show orders list + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: orders.length, + padding: EdgeInsets.zero, + separatorBuilder: (BuildContext context, int index) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final order = orders[index]; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AncillaryOrderCard( + order: order, + isLoading: false, + isOrdersList: true, + onCheckIn: onCheckIn != null ? () => onCheckIn!(order) : null, + onViewDetails: onViewDetails != null ? () => onViewDetails!(order) : null, + )), + ), + ), + ); + }, + ); + } + + Widget _buildDefaultEmptyState(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any ancillary orders yet.".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } +} + +class AncillaryOrderCard extends StatelessWidget { + const AncillaryOrderCard({ + super.key, + required this.order, + this.isLoading = false, + this.isOrdersList = false, + this.onCheckIn, + this.onViewDetails, + }); + + final AncillaryOrderItem order; + final bool isLoading; + final bool isOrdersList; + final VoidCallback? onCheckIn; + final VoidCallback? onViewDetails; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + isOrdersList + ? SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Ancillary Orders".toText14(isBold: true), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).toShimmer2(isShow: isLoading).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ToDoPage())); + }), + SizedBox(height: 10.h), + // Doctor and Clinic Info + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!isLoading) ...[ + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png", + width: 63.w, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + SizedBox(width: 12.w), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor Name + if (order.doctorName != null || isLoading) (isLoading ? "Dr. John Smith" : order.doctorName!).toString().toText16(isBold: true, maxlines: 2).toShimmer2(isShow: isLoading), + + SizedBox(height: 4.h), + ], + ), + ), + ], + ), + SizedBox(height: 8.h), + // Chips for Appointment Info and Status + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + // projectName + if (order.projectName != null || isLoading) + AppCustomChipWidget( + labelText: order.projectName ?? '-', + ).toShimmer2(isShow: isLoading), + // orderNo + // if (order.orderNo != null || isLoading) + // AppCustomChipWidget( + // // icon: AppAssets.calendar, + // labelText: "${"Order# :".needTranslation}${order.orderNo ?? '-'}", + // ).toShimmer2(isShow: isLoading), + + // Appointment Date + if (order.appointmentDate != null || isLoading) + AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: isLoading ? "Date: Jan 20, 2024" : DateFormat('MMM dd, yyyy').format(order.appointmentDate!).needTranslation, + ).toShimmer2(isShow: isLoading), + + // Appointment Number + if (order.appointmentNo != null || isLoading) + AppCustomChipWidget( + labelText: isLoading ? "Appt# : 98765" : "Appt #: ${order.appointmentNo}".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Invoice Number + if (order.invoiceNo != null || isLoading) + AppCustomChipWidget( + labelText: isLoading ? "Invoice: 45678" : "Invoice: ${order.invoiceNo}".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Queued Status + if (order.isQueued == true || isLoading) + AppCustomChipWidget( + labelText: "Queued".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Check-in Available Status + if (order.isCheckInAllow == true || isLoading) + AppCustomChipWidget( + labelText: "Check-in Ready".needTranslation, + ).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Action Buttons + Row( + children: [ + // Check-in Button (if available) + if (order.isCheckInAllow == true || isLoading) + Expanded( + child: CustomButton( + text: "Check In".needTranslation, + onPressed: () { + if (isLoading) { + return; + } else if (onCheckIn != null) { + onCheckIn!(); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + height: 40.h, + ).toShimmer2(isShow: isLoading), + ), + + if (order.isCheckInAllow == true || isLoading) SizedBox(width: 8.w), + + // View Details Button + Expanded( + child: CustomButton( + text: "View Details".needTranslation, + onPressed: () { + if (isLoading) { + return; + } else if (onViewDetails != null) { + onViewDetails!(); + } + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + height: 40.h, + iconSize: 15.h, + ).toShimmer2(isShow: isLoading), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart new file mode 100644 index 0000000..ba2f94d --- /dev/null +++ b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart @@ -0,0 +1,274 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class AncillaryProceduresList extends StatelessWidget { + final List orders; + final Function(AncillaryOrderItem order)? onCheckIn; + final Function(AncillaryOrderItem order)? onViewDetails; + + const AncillaryProceduresList({ + super.key, + required this.orders, + this.onCheckIn, + this.onViewDetails, + }); + + @override + Widget build(BuildContext context) { + // Show empty state + if (orders.isEmpty) { + return _buildDefaultEmptyState(context); + } + + // Show orders list + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: orders.length, + itemBuilder: (context, index) { + final order = orders[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AncillaryOrderCard( + order: order, + isLoading: false, + onCheckIn: onCheckIn != null ? () => onCheckIn!(order) : null, + onViewDetails: onViewDetails != null ? () => onViewDetails!(order) : null, + )), + ), + ), + ); + }, + ); + } + + Widget _buildDefaultEmptyState(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any ancillary orders yet.".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } +} + +class AncillaryOrderCard extends StatelessWidget { + const AncillaryOrderCard({ + super.key, + required this.order, + this.isLoading = false, + this.onCheckIn, + this.onViewDetails, + }); + + final AncillaryOrderItem order; + final bool isLoading; + final VoidCallback? onCheckIn; + final VoidCallback? onViewDetails; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row with Order Number and Date + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + "Order #".needTranslation.toText14( + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), + SizedBox(width: 4.w), + (isLoading ? "12345" : "${order.orderNo ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading), + ], + ), + if (order.orderDate != null || isLoading) + (isLoading ? "Jan 15, 2024" : DateFormat('MMM dd, yyyy').format(order.orderDate!)) + .toText12(color: AppColors.textColorLight) + .toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Doctor and Clinic Info + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor Name + if (order.doctorName != null || isLoading) + (isLoading ? "Dr. John Smith" : order.doctorName!) + .toString() + .toText14(isBold: true, maxlines: 2) + .toShimmer2(isShow: isLoading), + + SizedBox(height: 4.h), + + // Clinic Name + if (order.clinicName != null || isLoading) + (isLoading ? "Cardiology Clinic" : order.clinicName!) + .toString() + .toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + maxLine: 2, + ) + .toShimmer2(isShow: isLoading), + ], + ), + ), + ], + ), + + SizedBox(height: 12.h), + + // Chips for Appointment Info and Status + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + // Appointment Date + if (order.appointmentDate != null || isLoading) + AppCustomChipWidget( + icon: AppAssets.calendar, + labelText: + isLoading ? "Date: Jan 20, 2024" : "Date: ${DateFormat('MMM dd, yyyy').format(order.appointmentDate!)}".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Appointment Number + if (order.appointmentNo != null || isLoading) + AppCustomChipWidget( + labelText: isLoading ? "Appt #: 98765" : "Appt #: ${order.appointmentNo}".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Invoice Number + if (order.invoiceNo != null || isLoading) + AppCustomChipWidget( + labelText: isLoading ? "Invoice: 45678" : "Invoice: ${order.invoiceNo}".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Queued Status + if (order.isQueued == true || isLoading) + AppCustomChipWidget( + labelText: "Queued".needTranslation, + ).toShimmer2(isShow: isLoading), + + // Check-in Available Status + if (order.isCheckInAllow == true || isLoading) + AppCustomChipWidget( + labelText: "Check-in Ready".needTranslation, + ).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Action Buttons + Row( + children: [ + // Check-in Button (if available) + if (order.isCheckInAllow == true || isLoading) + Expanded( + child: CustomButton( + text: "Check In".needTranslation, + onPressed: () { + if (isLoading) { + return; + } else if (onCheckIn != null) { + onCheckIn!(); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + height: 40.h, + ).toShimmer2(isShow: isLoading), + ), + + if (order.isCheckInAllow == true || isLoading) SizedBox(width: 8.w), + + // View Details Button + Expanded( + child: CustomButton( + text: "View Details".needTranslation, + onPressed: () { + if (isLoading) { + return; + } else if (onViewDetails != null) { + onViewDetails!(); + } + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + height: 40.h, + icon: AppAssets.arrow_forward, + iconColor: AppColors.primaryRedColor, + iconSize: 15.h, + ).toShimmer2(isShow: isLoading), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart new file mode 100644 index 0000000..bd25641 --- /dev/null +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -0,0 +1,321 @@ +import 'dart:ui'; + +import 'package:flutter/material.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.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/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:provider/provider.dart'; + +class VitalSignPage extends StatefulWidget { + const VitalSignPage({super.key}); + + @override + State createState() => _VitalSignPageState(); +} + +class _VitalSignPageState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: 'Vital Signs', + child: Consumer( + builder: (context, viewModel, child) { + + // Get the latest vital sign data (first item in the list) + VitalSignResModel? latestVitalSign = viewModel.vitalSignList.isNotEmpty + ? viewModel.vitalSignList.first + : null; + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + + // Main content with body image + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left side - Vital Sign Cards + Expanded( + child: Column( + children: [ + // BMI Card + _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: 'BMI', + value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', + unit: '', + status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Height Card + _buildVitalSignCard( + icon: AppAssets.heightVital, + label: 'Height', + value: latestVitalSign?.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Weight Card + _buildVitalSignCard( + icon: AppAssets.weightVital, + label: 'Weight', + value: latestVitalSign?.weightKg?.toString() ?? '--', + unit: 'kg', + status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Blood Pressure Card + _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: 'Blood Pressure', + value: latestVitalSign != null && + latestVitalSign.bloodPressureHigher != null && + latestVitalSign.bloodPressureLower != null + ? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}' + : '--', + unit: '', + status: VitalSignUiModel.bloodPressureStatus( + systolic: latestVitalSign?.bloodPressureHigher, + diastolic: latestVitalSign?.bloodPressureLower, + ), + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Temperature Card + _buildVitalSignCard( + icon: AppAssets.temperature, + label: 'Temperature', + value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', + unit: '°C', + status: null, + onTap: () {}, + ), + ], + ), + ), + + SizedBox(width: 16.h), + + // Right side - Body Image and Heart Rate + Respiratory Rate + Expanded( + child: Column( + children: [ + // Body anatomy image with Heart Rate card overlaid at bottom + SizedBox( + height: 480.h, + width: double.infinity, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Image + Positioned.fill( + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + AppAssets.bmiFullBody, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 420.h, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.whiteColor.withValues(alpha: 0.0), + AppColors.whiteColor.withValues(alpha: 0.97), + AppColors.whiteColor, + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + + // Overlay Heart Rate card + Positioned( + left: 0, + right: 0, + bottom: 12.h, + child: _buildVitalSignCard( + icon: AppAssets.heart, + label: 'Heart Rate', + value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), + + // Respiratory rate Card + _buildVitalSignCard( + icon: AppAssets.respRate, + label: 'Respiratory rate', + value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 60.h), + ], + ), + ); + }, + ), + ), + ); + } + + Widget _buildVitalSignCard({ + required String icon, + required String label, + required String value, + required String unit, + required String? status, + required VoidCallback onTap, + }) { + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 14.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], + ), + ), + SizedBox(height: 8.h), + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 18.w, + height: 18.h, + iconColor: AppColors.textColorLight, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/water_monitor/water_consumption_screen.dart b/lib/presentation/water_monitor/water_consumption_screen.dart new file mode 100644 index 0000000..5d08bcd --- /dev/null +++ b/lib/presentation/water_monitor/water_consumption_screen.dart @@ -0,0 +1,840 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/hydration_tips_widget.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_intake_summary_widget.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class WaterConsumptionScreen extends StatefulWidget { + const WaterConsumptionScreen({super.key}); + + @override + State createState() => _WaterConsumptionScreenState(); +} + +class _WaterConsumptionScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _refreshData(); + }); + } + + /// Refresh data by calling initialize on the view model + Future _refreshData() async { + final vm = context.read(); + await vm.initialize(); + } + + Widget _buildLoadingShimmer({bool isForHistory = true}) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: isForHistory ? 60.h : 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget buildHistoryListTile({required String title, required String subTitle}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(weight: FontWeight.w500, color: AppColors.labelTextColor), + subTitle.toText18(weight: FontWeight.w600), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon) + ], + ).paddingSymmetrical(0, 8.h); + } + + Widget _buildHistoryGraphOrList() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Consumer(builder: (BuildContext context, WaterMonitorViewModel viewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "History".needTranslation.toText16(isBold: true), + Row( + children: [ + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), + SizedBox(width: 8.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Container( + padding: EdgeInsets.symmetric(vertical: 6.h, horizontal: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + backgroundColor: AppColors.greyColor, + borderRadius: 8.r, + hasShadow: true, + ), + child: Row( + children: [ + viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + ], + ), + ), + ) + ], + ), + ], + ), + if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] + ], + ); + }), + ); + } + + Widget _buildHistoryListView(WaterMonitorViewModel viewModel) { + final selectedDuration = viewModel.selectedDurationFilter; + + // Build list items based on duration + List listItems = []; + + if (selectedDuration == 'Daily') { + if (viewModel.todayProgressList.isNotEmpty) { + final todayData = viewModel.todayProgressList.first; + listItems.add( + buildHistoryListTile( + title: "Today's Progress", + subTitle: "${todayData.quantityConsumed?.toStringAsFixed(0) ?? '0'} ml / ${todayData.quantityLimit?.toStringAsFixed(0) ?? '0'} ml", + ), + ); + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + listItems.add( + buildHistoryListTile( + title: "Percentage Completed", + subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + + // Add history data if available (show ALL entries) + if (viewModel.historyList.isNotEmpty) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + listItems.add( + Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: "Water Intake History".toText14( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ); + + // Show all history entries + for (var history in viewModel.historyList) { + final quantity = "${history.quantity?.toStringAsFixed(0) ?? '0'} ml"; + final time = _formatHistoryDate(history.createdDate ?? ''); + + listItems.add( + buildHistoryListTile( + title: quantity, + subTitle: time, + ), + ); + + if (history != viewModel.historyList.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for today".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Weekly') { + if (viewModel.weekProgressList.isNotEmpty) { + // Show previous 6 days + today (total 7 days) + // API returns data in reverse order (today first), so we reverse it to show oldest to newest (top to bottom) + // This ensures today appears at the end (bottom) + final totalDays = viewModel.weekProgressList.length; + final startIndex = totalDays > 7 ? totalDays - 7 : 0; + final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList(); + + for (var dayData in weekDataToShow) { + listItems.add( + buildHistoryListTile( + title: dayData.dayName ?? 'Unknown', + subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (dayData != weekDataToShow.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this week".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Monthly') { + if (viewModel.monthProgressList.isNotEmpty) { + // Show last 6 months + current month (total 7 months) + // Show in chronological order: oldest to newest (top to bottom) + final totalMonths = viewModel.monthProgressList.length; + final startIndex = totalMonths > 7 ? totalMonths - 7 : 0; + final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList(); + + for (var monthData in monthDataToShow) { + listItems.add( + buildHistoryListTile( + title: monthData.monthName ?? 'Unknown', + subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (monthData != monthDataToShow.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this year".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } + + // Return scrollable list with min and max height constraints + return ConstrainedBox( + constraints: BoxConstraints(minHeight: 80.h, maxHeight: 270.h), + child: viewModel.isLoading + ? _buildLoadingShimmer().paddingOnly(top: 16.h) + : listItems.isEmpty + ? Center( + child: "No history data available".toText14(color: AppColors.greyTextColor), + ) + : ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + itemCount: listItems.length, + separatorBuilder: (context, index) => SizedBox.shrink(), + itemBuilder: (context, index) => listItems[index], + ), + ); + } + + Widget _buildHistoryGraph() { + return Consumer( + builder: (context, viewModel, _) { + final selectedDuration = viewModel.selectedDurationFilter; + + // Build dynamic data points based on selected duration + List dataPoints = []; + + if (selectedDuration == 'Daily') { + // For daily, show last 7 history entries with at least 5 minutes difference + if (viewModel.historyList.isNotEmpty) { + // Filter entries with at least 5 minutes difference + List filteredPoints = []; + DateTime? lastTime; + + for (var historyItem in viewModel.historyList) { + final currentTime = _parseHistoryDate(historyItem.createdDate ?? ''); + + // Add if first entry OR if more than 5 minutes difference from last added entry + if (lastTime == null || currentTime.difference(lastTime).inMinutes.abs() >= 5) { + final quantity = historyItem.quantity?.toDouble() ?? 0.0; + final time = _formatHistoryDate(historyItem.createdDate ?? ''); + + filteredPoints.add( + DataPoint( + value: quantity, + actualValue: quantity.toStringAsFixed(0), + label: time, + displayTime: time, + unitOfMeasurement: 'ml', + time: currentTime, + ), + ); + lastTime = currentTime; + } + } + + // Take only last 7 filtered entries + final totalFiltered = filteredPoints.length; + final startIndex = totalFiltered > 7 ? totalFiltered - 7 : 0; + dataPoints = filteredPoints.skip(startIndex).toList(); + } else if (viewModel.todayProgressList.isNotEmpty) { + // Fallback: show today's percentage if no history + final todayData = viewModel.todayProgressList.first; + final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: 'Today', + displayTime: 'Today', + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } else if (selectedDuration == 'Weekly') { + // For weekly, show previous 6 days + today (total 7 days) + // API returns data in reverse order (today first), so we reverse it to show oldest to newest (left to right) + // This ensures today appears at the end (right side) + if (viewModel.weekProgressList.isNotEmpty) { + final totalDays = viewModel.weekProgressList.length; + final startIndex = totalDays > 7 ? totalDays - 7 : 0; + final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList(); + + for (var dayData in weekDataToShow) { + final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0; + final dayName = dayData.dayName ?? 'Day ${dayData.dayNumber}'; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: DateUtil.getShortWeekDayName(dayName), + displayTime: dayName, + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } + } else if (selectedDuration == 'Monthly') { + // For monthly, show last 6 months + current month (total 7 months) + // Show in chronological order: oldest to newest (left to right) + if (viewModel.monthProgressList.isNotEmpty) { + final totalMonths = viewModel.monthProgressList.length; + final startIndex = totalMonths > 7 ? totalMonths - 7 : 0; + final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList(); + + for (var monthData in monthDataToShow) { + final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? 'Month ${monthData.monthNumber}'; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: DateUtil.getShortMonthName(monthName), + displayTime: monthName, + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } + } + + // If no data, show empty state + if (dataPoints.isEmpty) { + return Container( + padding: EdgeInsets.symmetric(vertical: 80.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.graphIcon, + iconColor: AppColors.greyTextColor.withValues(alpha: 0.5), + height: 32.w, + width: 32.w, + ), + SizedBox(height: 12.h), + "No graph data available".toText14(color: AppColors.greyTextColor), + ], + ), + ), + ); + } + + // Show loading shimmer while fetching data + if (viewModel.isLoading) { + return Container( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: _buildLoadingShimmer(), + ); + } + + // Configure graph based on selected duration + double maxY; + double minY; + double horizontalInterval; + double leftLabelInterval; + + if (selectedDuration == 'Daily') { + // For daily (quantity in ml), use max available cup size + // Get the biggest cup from available cups + final maxCupSize = viewModel.cups.isEmpty ? 500.0 : viewModel.cups.map((cup) => cup.capacityMl.toDouble()).reduce((a, b) => a > b ? a : b); + + maxY = maxCupSize; + minY = 0; + // Divide into 4 intervals (5 labels: 0, 1/4, 1/2, 3/4, max) + horizontalInterval = maxY / 4; + leftLabelInterval = maxY / 4; + } else { + // For weekly/monthly (percentage), use 0-100% + maxY = 100.0; + minY = 0; + horizontalInterval = 25; + leftLabelInterval = 25; + } + + return CustomGraph( + bottomLabelReservedSize: 30, + dataPoints: dataPoints, + makeGraphBasedOnActualValue: true, + leftLabelReservedSize: 50.h, + showGridLines: true, + maxY: maxY, + minY: minY, + showLinePoints: true, + maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0, + horizontalInterval: horizontalInterval, + leftLabelInterval: leftLabelInterval, + showShadow: true, + getDrawingHorizontalLine: (value) { + // Draw dashed lines at intervals + if (selectedDuration == 'Daily') { + // For daily, draw lines every 50 or 100 ml + if (value % horizontalInterval == 0 && value > 0) { + return FlLine( + color: AppColors.greyTextColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + } else { + // For weekly/monthly, draw lines at 25%, 50%, 75% + if (value == 25 || value == 50 || value == 75) { + return FlLine( + color: AppColors.successColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + } + return FlLine(color: AppColors.transparent, strokeWidth: 0); + }, + leftLabelFormatter: (value) { + if (selectedDuration == 'Daily') { + // Show exactly 5 labels: 0, 1/4, 1/2, 3/4, max + // Check if value matches one of the 5 positions + final interval = maxY / 4; + final positions = [0.0, interval, interval * 2, interval * 3, maxY]; + + for (var position in positions) { + if ((value - position).abs() < 1) { + return '${value.toInt()}ml'.toText10(weight: FontWeight.w600); + } + } + } else { + // Show percentage labels + if (value == 0) return '0%'.toText10(weight: FontWeight.w600); + if (value == 25) return '25%'.toText10(weight: FontWeight.w600); + if (value == 50) return '50%'.toText10(weight: FontWeight.w600); + if (value == 75) return '75%'.toText10(weight: FontWeight.w600); + if (value == 100) return '100%'.toText10(weight: FontWeight.w600); + } + return SizedBox.shrink(); + }, + graphColor: AppColors.successColor, + graphShadowColor: AppColors.successColor.withValues(alpha: 0.15), + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return SizedBox.shrink(); + + // Only show labels for whole number positions (not fractional) + if ((value - value.round()).abs() > 0.01) { + return SizedBox.shrink(); + } + + int index = value.round(); + if (index < 0 || index >= data.length) return SizedBox.shrink(); + + // For daily, show all 7 time labels (last 7 entries) + if (selectedDuration == 'Daily' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText8( + fontWeight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ); + } + + // For weekly, show all 7 days (today + last 6 days) + if (selectedDuration == 'Weekly' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10( + weight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ); + } + + // For monthly, show all 7 months (current month + last 6 months) + if (selectedDuration == 'Monthly' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + + return SizedBox.shrink(); + }, + scrollDirection: selectedDuration == 'Monthly' ? Axis.horizontal : Axis.vertical, + height: 250.h, + spotColor: AppColors.successColor, + ); + }, + ); + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + final dialogService = getIt.get(); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showHistoryDurationBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Duration".needTranslation, + items: viewModel.durationFilters, + selectedValue: viewModel.selectedDurationFilter, + onSelected: viewModel.setFilterDuration, + ); + } + + /// Handle reminder button tap (Set or Cancel) + Future _handleReminderButtonTap(WaterMonitorViewModel viewModel) async { + if (viewModel.isWaterReminderEnabled) { + // Cancel reminders + _showCancelReminderConfirmation(viewModel); + } else { + // Set reminders + await _setReminders(viewModel); + } + } + + /// Show confirmation bottom sheet before cancelling reminders + void _showCancelReminderConfirmation(WaterMonitorViewModel viewModel) { + showCommonBottomSheetWithoutHeight( + title: 'Notice'.needTranslation, + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to cancel all water reminders?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + await _cancelReminders(viewModel); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + isDismissible: true, + ); + } + + /// Set water reminders + Future _setReminders(WaterMonitorViewModel viewModel) async { + // Schedule reminders + final success = await viewModel.scheduleWaterReminders(); + + if (success) { + final times = await viewModel.getScheduledReminderTimes(); + _showReminderScheduledDialog(times); + } + } + + /// Cancel water reminders + Future _cancelReminders(WaterMonitorViewModel viewModel) async { + final success = await viewModel.cancelWaterReminders(); + } + + /// Show bottom sheet with scheduled reminder times + void _showReminderScheduledDialog(List times) { + showCommonBottomSheetWithoutHeight( + title: 'Reminders Set!'.needTranslation, + context, + isCloseButtonVisible: false, + isDismissible: false, + child: Padding( + padding: EdgeInsets.only(top: 16.w, left: 16.w, right: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.getSuccessWidget(loadingText: 'Daily water reminders scheduled at:'.needTranslation), + SizedBox(height: 16.h), + Wrap( + spacing: 8.w, + runSpacing: 8.h, + children: times + .map( + (time) => AppCustomChipWidget( + icon: AppAssets.bell, + iconColor: AppColors.quickLoginColor, + richText: _formatTime(time).toText14(), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), + ), + ) + .toList(), + ), + + SizedBox(height: 24.h), + + // OK button + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: 'OK'.needTranslation, + onPressed: () => Navigator.of(context).pop(), + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + callBackFunc: () {}, + ); + } + + /// Format DateTime to readable time string + String _formatTime(DateTime time) { + final hour = time.hour; + final minute = time.minute; + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period'; + } + + /// Format history date from /Date(milliseconds+0300)/ format + String _formatHistoryDate(String dateString) { + try { + // Parse the /Date(milliseconds+0300)/ format + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dateString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds); + return _formatTime(dateTime); + } + } + } catch (e) { + return dateString; + } + return dateString; + } + + /// Parse history date from /Date(milliseconds+0300)/ format to DateTime + DateTime _parseHistoryDate(String dateString) { + try { + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dateString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + return DateTime.fromMillisecondsSinceEpoch(milliseconds); + } + } + } catch (e) { + // Return current time as fallback + } + return DateTime.now(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Water Consumption".needTranslation, + bottomChild: Consumer( + builder: (context, viewModel, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: viewModel.isWaterReminderEnabled ? "Cancel Reminders".needTranslation : "Set Reminder".needTranslation, + textColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor, + backgroundColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor.withValues(alpha: 0.1) : AppColors.successLightBgColor, + onPressed: () => _handleReminderButtonTap(viewModel), + icon: viewModel.isWaterReminderEnabled ? null : AppAssets.bell, + iconColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor, + borderRadius: 12.r, + borderColor: AppColors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ), + child: RefreshIndicator( + onRefresh: _refreshData, + color: AppColors.blueColor, + backgroundColor: AppColors.whiteColor, + child: Column( + children: [ + SizedBox(height: 16.h), + const WaterIntakeSummaryWidget(), + SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), + const HydrationTipsWidget(), + SizedBox(height: 16.h), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_screen.dart new file mode 100644 index 0000000..1b783ca --- /dev/null +++ b/lib/presentation/water_monitor/water_monitor_settings_screen.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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'; +import 'package:provider/provider.dart'; + +class WaterMonitorSettingsScreen extends StatefulWidget { + const WaterMonitorSettingsScreen({super.key}); + + @override + State createState() => _WaterMonitorSettingsScreenState(); +} + +class _WaterMonitorSettingsScreenState extends State { + // No need to call initialize() here since it's already called in water_consumption_screen + // The ViewModel is shared via Provider, so data is already loaded + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + // Reusable method to show selection bottom sheet + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + final dialogService = getIt.get(); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showGenderSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Gender".needTranslation, + items: viewModel.genderOptions, + selectedValue: viewModel.selectedGender, + onSelected: viewModel.setGender, + ); + } + + void _showHeightUnitSelectionBottomSheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.heightUnits, + selectedValue: viewModel.selectedHeightUnit, + onSelected: viewModel.setHeightUnit, + useUpperCase: true, + ); + } + + void _showWeightUnitSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.weightUnits, + selectedValue: viewModel.selectedWeightUnit, + onSelected: viewModel.setWeightUnit, + useUpperCase: true, + ); + } + + void _showActivityLevelSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Activity Level".needTranslation, + items: viewModel.activityLevels, + selectedValue: viewModel.selectedActivityLevel, + onSelected: viewModel.setActivityLevel, + ); + } + + void _showNumberOfRemindersSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Number of Reminders".needTranslation, + items: viewModel.reminderOptions, + selectedValue: viewModel.selectedNumberOfReminders, + onSelected: viewModel.setNumberOfReminders, + ); + } + + // Reusable method to build text field + Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: 1, + cursorHeight: 14.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hintText, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ); + } + + // Reusable method to build settings row + Widget _buildSettingsRow({ + required String icon, + required String label, + String? value, + Widget? inputField, + String? unit, + VoidCallback? onUnitTap, + VoidCallback? onRowTap, + bool showDivider = true, + }) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 40.w, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.greyColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w)), + ), + SizedBox(width: 12.w), + Expanded( + flex: unit != null ? 3 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + if (inputField != null) + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: inputField, + ) + else if (value != null) + value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ), + if (unit != null) ...[ + Container( + width: 1.w, + height: 30.w, + color: AppColors.dividerColor, + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(onUnitTap ?? () {}), + ), + ] else if (onRowTap != null) ...[ + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ], + ).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}), + if (showDivider) Divider(height: 1, color: AppColors.dividerColor), + ], + ); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "H20 Settings".needTranslation, + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Save".needTranslation, + onPressed: () async { + final success = await viewModel.saveSettings(); + if (!success && viewModel.validationError != null) { + _showSnackBar(context, viewModel.validationError!); + } else if (success) { + _showSnackBar(context, "Settings saved successfully"); + } + }, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildSettingsRow( + icon: AppAssets.profileIcon, + label: "Your Name".needTranslation, + inputField: _buildTextField(viewModel.nameController, 'Guest'), + ), + _buildSettingsRow( + icon: AppAssets.genderIcon, + label: "Select Gender".needTranslation, + value: viewModel.selectedGender, + onRowTap: () => _showGenderSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.calendarGrey, + label: "Age (11-120) yrs".needTranslation, + inputField: _buildTextField( + viewModel.ageController, + '20', + keyboardType: TextInputType.number, + ), + ), + _buildSettingsRow( + icon: AppAssets.heightIcon, + label: "Height".needTranslation, + inputField: _buildTextField( + viewModel.heightController, + '175', + keyboardType: TextInputType.number, + ), + unit: viewModel.selectedHeightUnit, + onUnitTap: () => _showHeightUnitSelectionBottomSheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.weightScaleIcon, + label: "Weight".needTranslation, + inputField: _buildTextField( + viewModel.weightController, + '75', + keyboardType: TextInputType.number, + ), + unit: viewModel.selectedWeightUnit, + onUnitTap: () => _showWeightUnitSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.dumbellIcon, + label: "Activity Level".needTranslation, + value: viewModel.selectedActivityLevel, + onRowTap: () => _showActivityLevelSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.notificationIconGrey, + label: "Number of reminders in a day".needTranslation, + value: viewModel.selectedNumberOfReminders, + onRowTap: () => _showNumberOfRemindersSelectionBottomsheet(context, viewModel), + showDivider: false, + ), + ], + ), + ), + ), + ); + } + + // Show snackbar for validation errors and success messages + void _showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 3), + behavior: SnackBarBehavior.floating, + backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor, + ), + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart b/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart new file mode 100644 index 0000000..b4b7ba0 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart @@ -0,0 +1,25 @@ +// Add this class at the bottom of your file (outside the main class) +import 'package:flutter/material.dart'; + +class BottleShapeClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + + // Create rounded rectangle matching the bottle body shape + // The bottle has rounded corners with radius ~30-40 based on SVG + final borderRadius = size.width * 0.25; // 25% of width for rounded corners + + path.addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + Radius.circular(borderRadius), + ), + ); + + return path; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) => false; +} diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart new file mode 100644 index 0000000..4ffa30d --- /dev/null +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -0,0 +1,325 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; + +/// Bottom sheet to switch between existing cups or add new custom cup +void showSwitchCupBottomSheet(BuildContext context) { + return showCommonBottomSheetWithoutHeight( + context, + titleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Switch Cups".toText20(weight: FontWeight.w600), + "Select your preferred cup size".toText16(weight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + child: SwitchCupBottomSheet(), + callBackFunc: () {}, + ); +} + +class SwitchCupBottomSheet extends StatelessWidget { + const SwitchCupBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + final selectedId = viewModel.selectedCup?.id; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 16.w, + childAspectRatio: 0.85, + ), + itemCount: viewModel.cups.length + 1, + itemBuilder: (context, index) { + if (index == viewModel.cups.length) { + return _buildAddCupItem(context); + } + + final cup = viewModel.cups[index]; + final isSelected = selectedId == cup.id; + + return _buildCupItem( + cup: cup, + isSelected: isSelected, + onTap: () { + viewModel.selectCup(cup.id); + context.pop(); + }, + ); + }, + ), + ], + ); + } + + Widget _buildCupItem({required WaterCupModel cup, required bool isSelected, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, + width: 1, + ), + ), + child: Center( + child: Utils.buildSvgWithAssets( + icon: cup.iconPath, + height: 30.h, + width: 42.w, + iconColor: isSelected ? AppColors.primaryRedColor : null, + ), + ), + ), + // Red badge for custom cups (delete) + if (!cup.isDefault) + Positioned( + top: -6.h, + right: -6.w, + child: Builder(builder: (ctx) { + return InkWell( + onTap: () { + // call viewmodel remove + final vm = ctx.read(); + vm.removeCup(cup.id); + }, + child: Container( + color: AppColors.whiteColor, + child: Utils.buildSvgWithAssets(icon: AppAssets.minimizeIcon, height: 20.w, width: 20.w), + ), + ); + }), + ), + ], + ), + SizedBox(height: 2.h), + '${cup.capacityMl}ml'.toText10(weight: FontWeight.w500), + ], + ), + ); + } + + Widget _buildAddCupItem(BuildContext context) { + return GestureDetector( + onTap: () { + showCustomizeCupBottomSheet(context); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 60.w, + height: 60.w, + child: Center(child: Utils.buildSvgWithAssets(icon: AppAssets.cupAdd, height: 30.h, width: 42.w)), + ), + SizedBox(height: 4.h), + 'Add'.needTranslation.toText10(weight: FontWeight.w500), + ], + ), + ); + } +} + +/// Bottom sheet to customize cup capacity with slider +void showCustomizeCupBottomSheet(BuildContext context, {WaterCupModel? cupToEdit}) { + return showCommonBottomSheetWithoutHeight( + context, + titleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Customize your drink cup".needTranslation.toText20(weight: FontWeight.w600), + ], + ), + child: CustomizeCupBottomSheet(cupToEdit: cupToEdit), + callBackFunc: () {}, + ); +} + +class CustomizeCupBottomSheet extends StatefulWidget { + final WaterCupModel? cupToEdit; + + const CustomizeCupBottomSheet({super.key, this.cupToEdit}); + + @override + State createState() => _CustomizeCupBottomSheetState(); +} + +class _CustomizeCupBottomSheetState extends State { + static const int minCapacity = 50; + static const int maxCapacity = 500; + + late double _currentCapacity; + + @override + void initState() { + super.initState(); + _currentCapacity = (widget.cupToEdit?.capacityMl ?? 150).toDouble(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Cup icon with fill level visualization + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 60.w, + height: 80.h, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Cup image with dynamic fill + SizedBox( + width: 60.w, + height: 80.h, + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.cupEmpty, + width: 60.w, + height: 80.h, + fit: BoxFit.contain, + ), + ), + ), + ClipRect( + child: Align( + alignment: Alignment.bottomCenter, + heightFactor: (_currentCapacity / maxCapacity).clamp(0.0, 1.0), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cupFilled, + width: 60.w, + height: 80.h, + fit: BoxFit.contain, + )), + ), + ], + ), + ), + + SizedBox(width: 12.w), + + // Slider and labels + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current value + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + '${_currentCapacity.round()}'.toText32(isBold: true), + SizedBox(width: 4.w), + Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Text( + 'ml', + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + height: 1.0, + ), + ), + ), + ], + ).paddingOnly(left: 12.w), + + SizedBox(height: 16.h), + + // Slider + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppColors.primaryRedColor, + inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2), + thumbColor: AppColors.primaryRedColor, + overlayColor: AppColors.primaryRedColor.withOpacity(0.2), + trackHeight: 4.h, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), + ), + child: Slider( + value: _currentCapacity, + min: minCapacity.toDouble(), + max: maxCapacity.toDouble(), + divisions: (maxCapacity - minCapacity) ~/ 10, + onChanged: (value) => setState(() => _currentCapacity = value), + ), + ), + + Align( + alignment: Alignment.centerRight, + child: 'Max: $maxCapacity ml'.toText14( + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ], + ), + + SizedBox(height: 24.h), + CustomButton( + text: 'Select'.needTranslation, + onPressed: () { + final newCup = WaterCupModel( + id: widget.cupToEdit?.id ?? Uuid().v4(), + name: '${_currentCapacity.round()}ml', + capacityMl: _currentCapacity.round(), + iconPath: AppAssets.cupEmpty, + isDefault: false, + ); + + final viewModel = context.read(); + if (widget.cupToEdit != null) { + viewModel.updateCup(newCup); + } else { + viewModel.addCup(newCup); + } + viewModel.selectCup(newCup.id); + + Navigator.pop(context); + Navigator.pop(context); + }, + backgroundColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ], + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart b/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart new file mode 100644 index 0000000..df55886 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HydrationTipsWidget extends StatelessWidget { + const HydrationTipsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.bulb_icon, + width: 24.w, + height: 24.h, + ), + SizedBox(width: 8.w), + "Tips to stay hydrated".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(height: 8.h), + " • ${"Drink before you feel thirsty"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Keep a refillable bottle next to you"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Track your daily intake to stay motivated"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Choose sparkling water instead of soda"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 8.h), + ], + ), + ); + } +} + diff --git a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart new file mode 100644 index 0000000..6546f77 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +class WaterActionButtonsWidget extends StatelessWidget { + const WaterActionButtonsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, vm, _) { + final cupAmount = vm.selectedCupCapacityMl; + final isGoalAchieved = vm.progressPercent >= 100 || vm.nextDrinkTime.toLowerCase().contains('goal achieved'); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () async { + if (cupAmount > 0) { + await vm.undoUserActivity(); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.minimizeIcon, + height: 20.h, + width: 20.h, + iconColor: AppColors.textColor, + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 99.r, + hasShadow: true, + ), + child: (cupAmount > 0 ? "+ $cupAmount ml" : "+ 0ml").toText12( + fontWeight: FontWeight.w600, + color: AppColors.whiteColor, + ), + ), + Opacity( + opacity: isGoalAchieved ? 0.4 : 1.0, + child: InkWell( + onTap: isGoalAchieved + ? null + : () async { + if (cupAmount > 0) { + await vm.insertUserActivity(quantityIntake: cupAmount); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.addIconDark, + height: 20.h, + width: 20.h, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildActionButton( + context: context, + onTap: () => showSwitchCupBottomSheet(context), + overlayWidget: AppAssets.refreshIcon, + title: "Switch Cup".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + context: context, + onTap: () async {}, + title: "Plain Water".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + context: context, + onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen), + title: "Settings".needTranslation, + icon: Icon( + Icons.settings, + color: AppColors.blueColor, + size: 24.w, + ), + ), + ], + ), + ], + ); + }); + } + + Widget _buildActionButton({ + required BuildContext context, + String? overlayWidget, + required String title, + required Widget icon, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Column( + children: [ + Stack( + children: [ + Container( + height: 46.w, + width: 46.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor.withValues(alpha: 0.14), + borderRadius: 12.r, + hasShadow: true, + ), + child: Center(child: icon), + ), + if (overlayWidget != null) ...[ + Positioned( + top: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(2.w), + height: 16.w, + width: 16.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 100.r, + hasShadow: true, + ), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.refreshIcon, + iconColor: AppColors.whiteColor, + ), + ), + ), + ), + ] + ], + ), + SizedBox(height: 4.h), + title.toText10(), + ], + ), + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_bottle_widget.dart b/lib/presentation/water_monitor/widgets/water_bottle_widget.dart new file mode 100644 index 0000000..b681e32 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_bottle_widget.dart @@ -0,0 +1,112 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/bottle_shape_clipper.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_consumption_progress_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +class WaterBottleWidget extends StatelessWidget { + const WaterBottleWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, vm, _) { + final progressPercent = (vm.progress * 100).clamp(0.0, 100.0); + + // SVG aspect ratio + const svgAspectRatio = 315.0 / 143.0; // ~2.2 + + // Responsive bottle sizing with device-specific constraints + double bottleWidth; + if (isTablet) { + bottleWidth = math.min(SizeUtils.width * 0.15, 180.0); + } else if (isFoldable) { + bottleWidth = math.min(100.w, 160.0); + } else { + bottleWidth = math.min(120.w, 140.0); + } + + final bottleHeight = bottleWidth * svgAspectRatio; + + // Fillable area percentages + final fillableHeightPercent = 0.7; + const fillableWidthPercent = 0.8; + + final fillableHeight = bottleHeight * fillableHeightPercent; + final fillableWidth = bottleWidth * fillableWidthPercent; + + // Device-specific positioning offsets + final double leftOffset = isTablet ? 4.w : 8.w; + final double bottomOffset = isTablet ? -65.h : -78.h; + + return SizedBox( + height: bottleHeight, + width: bottleWidth, + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + // Bottle SVG outline + Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.waterBottle, + height: bottleHeight, + width: bottleWidth, + fit: BoxFit.contain, + ), + ), + + // Wave and bubbles clipped to bottle shape + Positioned.fill( + left: leftOffset, + bottom: bottomOffset, + child: Center( + child: SizedBox( + width: fillableWidth, + height: fillableHeight, + child: ClipPath( + clipper: BottleShapeClipper(), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Animated wave + Positioned( + child: WaterConsumptionProgressWidget( + progress: progressPercent, + size: math.min(fillableWidth, fillableHeight), + containerWidth: fillableWidth, + containerHeight: fillableHeight, + waveDuration: const Duration(milliseconds: 3000), + waveColor: AppColors.blueColor, + ), + ), + + // Bubbles (only show if progress > 10%) + if (progressPercent > 10) + Positioned( + bottom: fillableHeight * 0.12, + child: Utils.buildSvgWithAssets( + icon: AppAssets.waterBottleOuterBubbles, + height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22), + width: fillableWidth * 0.65, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart b/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart new file mode 100644 index 0000000..08350a1 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_splash_progress_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WaterConsumptionProgressWidget extends StatefulWidget { + /// progress: 0.0 - 100.0 + final double progress; + final double size; + final Color? waveColor; + final double? containerWidth; + final double? containerHeight; + final Duration? waveDuration; + + const WaterConsumptionProgressWidget({super.key, required this.progress, this.size = 100, this.waveColor, this.containerWidth, this.containerHeight, this.waveDuration}); + + @override + State createState() => _WaterConsumptionProgressWidgetState(); +} + +class _WaterConsumptionProgressWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _progressController; + late Animation _progressAnimation; + double _lastTarget = 0.0; + + @override + void initState() { + super.initState(); + _lastTarget = widget.progress; + _progressController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200)); + _progressAnimation = Tween(begin: 0, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut)); + _progressController.forward(); + } + + @override + void didUpdateWidget(covariant WaterConsumptionProgressWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if ((widget.progress - _lastTarget).abs() > 0.01) { + // animate from current value to new target + final begin = _progressAnimation.value; + _progressAnimation = + Tween(begin: begin, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut)); + _progressController + ..reset() + ..forward(); + _lastTarget = widget.progress; + } + } + + @override + void dispose() { + _progressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // default to app blue color if none provided + final blue = widget.waveColor ?? AppColors.blueColor; + return AnimatedBuilder( + animation: _progressAnimation, + builder: (context, child) { + return WaterWaveProgress( + progress: _progressAnimation.value, + size: widget.size, + showPercentage: false, + useCircleClip: false, + waveColor: blue, + containerWidth: widget.containerWidth, + containerHeight: widget.containerHeight, + waveDuration: widget.waveDuration, + backgroundColor: Colors.transparent, + progressColor: Colors.transparent, + textColor: Colors.transparent, + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart b/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart new file mode 100644 index 0000000..137f6a3 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_action_buttons_widget.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_bottle_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class WaterIntakeSummaryWidget extends StatelessWidget { + const WaterIntakeSummaryWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.blueGradientColorOne, + AppColors.blueGradientColorTwo, + ], + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: isTablet ? 2 : 3, + child: Consumer(builder: (context, vm, _) { + if (vm.isLoading) { + return _buildLoadingShimmer(); + } + + final goalMl = vm.dailyGoalMl; + final consumed = vm.totalConsumedMl; + final remaining = (goalMl - consumed) > 0 ? (goalMl - consumed) : 0; + final completedPercent = "${(vm.progress * 100).clamp(0.0, 100.0).toStringAsFixed(0)}%"; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Don't show label if goal is achieved + if (!vm.nextDrinkTime.toLowerCase().contains('goal achieved')) + // Show "Tomorrow" if nextDrinkTime contains "tomorrow", otherwise "Next Drink Time" + (vm.nextDrinkTime.toLowerCase().contains('tomorrow') ? "Tomorrow" : "Next Drink Time") + .needTranslation + .toText18(weight: FontWeight.w600, color: AppColors.textColor), + + // Extract only time if "tomorrow" is present, otherwise show as is + (vm.nextDrinkTime.toLowerCase().contains('tomorrow') + ? vm.nextDrinkTime.replaceAll(RegExp(r'tomorrow', caseSensitive: false), '').trim() + : vm.nextDrinkTime) + .toText32(weight: FontWeight.w600, color: AppColors.blueColor), + + SizedBox(height: 12.h), + _buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "${goalMl}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Remaining".needTranslation, subTitle: "${remaining}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Completed".needTranslation, subTitle: completedPercent, subTitleColor: AppColors.successColor), + SizedBox(height: 8.h), + _buildStatusColumn( + title: "Hydration Status".needTranslation, + subTitle: vm.hydrationStatus, + subTitleColor: vm.hydrationStatusColor, + ), + ], + ); + }), + ), + SizedBox(width: isTablet ? 32 : 16.w), + Expanded( + flex: isTablet ? 1 : 2, + child: const WaterBottleWidget(), + ), + ], + ), + const WaterActionButtonsWidget(), + ], + ), + ); + } + + Widget _buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "$title: ".toText16(weight: FontWeight.w500, color: AppColors.textColor), + subTitle.toText12( + fontWeight: FontWeight.w600, + color: subTitleColor ?? AppColors.greyTextColor, + ), + ], + ); + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart b/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart new file mode 100644 index 0000000..da5eb06 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart @@ -0,0 +1,351 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Spherical water wave progress bar with animated ripple effect +/// Based on https://github.com/meeziest/spherical_water_wavy_progress_bar +/// +/// Usage: +/// WaterWaveProgress( +/// progress: 75.0, +/// size: 200.w, +/// showPercentage: true, +/// ) +class WaterWaveProgress extends StatefulWidget { + final double progress; // 0.0 to 100.0 + final double size; + final bool showPercentage; + final bool useCircleClip; + final Color? waveColor; + final Color? backgroundColor; + final Color? progressColor; + final Color? textColor; + final Duration? waveDuration; + + // When drawing inside a non-square clip (like bottle area), provide the actual available + // width and height so the painter can compute vertical fill correctly. + final double? containerWidth; + final double? containerHeight; + + const WaterWaveProgress({ + super.key, + required this.progress, + this.size = 200, + this.showPercentage = true, + this.useCircleClip = true, + this.waveColor, + this.backgroundColor, + this.progressColor, + this.textColor, + this.waveDuration, + this.containerWidth, + this.containerHeight, + }); + + @override + State createState() => _WaterWaveProgressState(); +} + +class _WaterWaveProgressState extends State with SingleTickerProviderStateMixin { + late AnimationController _waveController; + + @override + void initState() { + super.initState(); + _waveController = AnimationController( + vsync: this, + duration: widget.waveDuration ?? const Duration(milliseconds: 2000), + )..repeat(); + } + + @override + void dispose() { + _waveController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final radius = widget.size / 2; + + // When container dimensions are provided (non-square bottle area), use them so + // the CustomPaint canvas matches the available clip rect. Otherwise fall back + // to the square size value. + final paintWidth = widget.containerWidth ?? widget.size; + final paintHeight = widget.containerHeight ?? widget.size; + + return SizedBox( + width: paintWidth, + height: paintHeight, + child: CustomPaint( + painter: _WavePainter( + progress: widget.progress, + waveAnimation: _waveController, + // For circle-based painters we still provide circleRadius; when using + // rectangular painting the circleRadius is unused. + circleRadius: radius, + waveColor: widget.waveColor ?? AppColors.primaryRedColor, + backgroundColor: widget.backgroundColor ?? AppColors.bgScaffoldColor, + useCircleClip: widget.useCircleClip, + containerWidth: widget.containerWidth, + containerHeight: widget.containerHeight, + ), + // Only draw the circular progress/percentage when explicitly requested. + foregroundPainter: widget.showPercentage + ? _ProgressPainter( + progress: widget.progress, + circleRadius: radius, + progressColor: widget.progressColor ?? AppColors.primaryRedColor, + textColor: widget.textColor ?? AppColors.textColor, + showPercentage: widget.showPercentage, + ) + : null, + ), + ); + } +} + +/// Paints the animated water waves inside a circular clip +class _WavePainter extends CustomPainter { + final double progress; + final Animation waveAnimation; + final double circleRadius; + final Color waveColor; + final Color backgroundColor; + final bool useCircleClip; + final double? containerWidth; + final double? containerHeight; + + _WavePainter({ + required this.progress, + required this.waveAnimation, + required this.circleRadius, + required this.waveColor, + required this.backgroundColor, + this.useCircleClip = true, + this.containerWidth, + this.containerHeight, + }) : super(repaint: waveAnimation); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + canvas.translate(center.dx, center.dy); + + if (useCircleClip) { + // Clip to circle + canvas.clipPath( + Path() + ..addOval( + Rect.fromCircle( + center: Offset.zero, + radius: circleRadius, + ), + ), + ); + + // Fill background circle if specified + if (backgroundColor != Colors.transparent) { + canvas.drawCircle( + Offset.zero, + circleRadius, + Paint()..color = backgroundColor, + ); + } + + // Draw two sine waves clipped to circle for spherical style + _drawSineWave(canvas, waveColor.withAlpha((0.5 * 255).round()), shift: 0); + _drawSineWave(canvas, waveColor, shift: circleRadius / 2, mirror: true); + } else { + // No circular clipping: draw waves over the full rectangular area. Use provided container + // dimensions if available; otherwise fall back to square bounds derived from circleRadius. + final w = containerWidth ?? (circleRadius * 2); + final h = containerHeight ?? (circleRadius * 2); + + // use local transforms to center at origin and compute waves relative to the provided rect + canvas.save(); + canvas.translate(-w / 2, -h / 2); // move origin to top-left of the rect + + _drawSineWaveRect(canvas, waveColor.withAlpha((0.5 * 255).round()), width: w, height: h, shift: 0); + _drawSineWaveRect(canvas, waveColor, width: w, height: h, shift: w / 4, mirror: true); + + canvas.restore(); + } + } + + void _drawSineWaveRect(Canvas canvas, Color color, {required double width, required double height, double shift = 0.0, bool mirror = false}) { + if (mirror) { + canvas.save(); + canvas.translate(width, 0); + canvas.scale(-1, 1); + } + + final amplitude = height * 0.04; // smaller amplitude for rectangular waves + final angularVelocity = pi / (width / 2); + final delta = Curves.easeInOut.transform(progress / 100); + + final offsetX = 2 * width * waveAnimation.value + shift; + final offsetY = height * (1.0 - delta); + + final path = Path(); + for (double x = 0; x <= width; x += 1) { + final y = amplitude * sin(angularVelocity * (x + offsetX)); + if (x == 0) { + path.moveTo(x, y + offsetY); + } else { + path.lineTo(x, y + offsetY); + } + } + + path.lineTo(width, height); + path.lineTo(0, height); + path.close(); + + final wavePaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + canvas.drawPath(path, wavePaint); + + if (mirror) canvas.restore(); + } + + void _drawSineWave(Canvas canvas, Color color, {double shift = 0.0, bool mirror = false}) { + if (mirror) { + canvas.save(); + canvas.transform(Matrix4.rotationY(pi).storage); + } + + // original circular/spherical style: compute bounds based on circleRadius + final startX = -circleRadius; + final endX = circleRadius; + final startY = circleRadius; + final endY = -circleRadius; + + final amplitude = circleRadius * 0.15; + final angularVelocity = pi / circleRadius; + final delta = Curves.easeInOut.transform(progress / 100); + + final offsetX = 2 * circleRadius * waveAnimation.value + shift; + final offsetY = startY + (endY - startY - amplitude) * delta; + + final wavePaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final path = Path(); + + for (double x = startX; x <= endX; x++) { + // Sine wave function: y = A * sin(ωx + φ) + final y = amplitude * sin(angularVelocity * (x + offsetX)); + if (x == startX) { + path.moveTo(x, y + offsetY); + } else { + path.lineTo(x, y + offsetY); + } + } + + path.lineTo(endX, startY); + path.lineTo(startX, startY); + path.close(); + + canvas.drawPath(path, wavePaint); + + if (mirror) canvas.restore(); + } + + @override + bool shouldRepaint(covariant _WavePainter oldDelegate) => oldDelegate.progress != progress; +} + +/// Paints the circular progress arc and percentage text +class _ProgressPainter extends CustomPainter { + final double progress; + final double circleRadius; + final Color progressColor; + final Color textColor; + final bool showPercentage; + + _ProgressPainter({ + required this.progress, + required this.circleRadius, + required this.progressColor, + required this.textColor, + required this.showPercentage, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + canvas.translate(center.dx, center.dy); + + _drawCircleProgress(canvas); + if (showPercentage) { + _drawProgressText(canvas); + } + } + + void _drawCircleProgress(Canvas canvas) { + final strokeWidth = circleRadius * 0.077; + + // Background circle with shadow + final bgPaint = Paint() + ..color = progressColor.withAlpha((0.2 * 255).round()) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + final shadowPaint = Paint() + ..color = Colors.black.withAlpha((0.1 * 255).round()) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..maskFilter = const MaskFilter.blur(BlurStyle.outer, 10); + + canvas.drawCircle(Offset.zero, circleRadius, bgPaint); + canvas.drawCircle(Offset.zero, circleRadius, shadowPaint); + + // Progress arc + final progressPaint = Paint() + ..color = progressColor + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + canvas.drawArc( + Rect.fromCircle(center: Offset.zero, radius: circleRadius), + -0.5 * pi, + 2 * pi * (progress / 100), + false, + progressPaint, + ); + } + + void _drawProgressText(Canvas canvas) { + final textSpan = TextSpan( + text: "${progress.toInt()}%", + style: TextStyle( + color: textColor, + fontSize: circleRadius * 0.35, + fontWeight: FontWeight.w700, + height: 1.0, + ), + ); + + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + )..layout(); + + textPainter.paint( + canvas, + Offset(-textPainter.width / 2, -textPainter.height / 2), + ); + } + + @override + bool shouldRepaint(covariant _ProgressPainter oldDelegate) => oldDelegate.progress != progress; +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 5a93216..78045b5 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,12 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; -import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; +import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; +import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/huawei_health_example.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_instructions_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/risk_factors_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/suggestions_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_selector_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_screen.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../presentation/covid19test/covid19_landing_page.dart'; + class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -14,6 +36,35 @@ class AppRoutes { static const String registerStepTwo = '/registerStepTwo'; static const String landingScreen = '/landingScreen'; static const String medicalFilePage = '/medicalFilePage'; + static const String eReferralPage = '/erReferralPage'; + static const String comprehensiveCheckupPage = '/comprehensiveCheckupPage'; + static const String healthCalculatorsPage = '/healthCalculatorsPage'; + static const String healthConvertersPage = '/healthConvertersPage'; + static const String homeHealthCarePage = '/homeHealthCarePage'; + static const String zoomCallPage = '/zoomCallPage'; + static const String bloodDonationPage = '/bloodDonationPage'; + static const String smartWatches = '/smartWatches'; + static const String huaweiHealthExample = '/huaweiHealthExample'; + static const String covid19Test = '/covid19Test'; + static const String vitalSign = '/vitalSign'; + //appointments + static const String bookAppointmentPage = '/bookAppointmentPage'; + + // Water Monitor + static const String waterConsumptionScreen = '/waterConsumptionScreen'; + static const String waterMonitorSettingsScreen = '/waterMonitorSettingsScreen'; + + // Symptoms Checker + static const String organSelectorPage = '/organSelectorPage'; + static const String symptomsSelectorScreen = '/symptomsCheckerScreen'; + static const String suggestionsScreen = '/suggestionsScreen'; + static const String riskFactorsScreen = '/riskFactorsScreen'; + static const String possibleConditionsScreen = '/possibleConditionsScreen'; + static const String triageScreen = '/triageProgressScreen'; + + //UserInfoSelection + static const String userInfoSelection = '/userInfoSelection'; + static const String userInfoFlowManager = '/userInfoFlowManager'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -21,6 +72,29 @@ class AppRoutes { landingScreen: (context) => LandingNavigation(), register: (context) => RegisterNew(), registerStepTwo: (context) => RegisterNewStep2(), - medicalFilePage: (context) => MedicalFilePage(), + medicalFilePage: (context) => MedicalFilePage(), + zoomCallPage: (context) => CallScreen(), + eReferralPage: (context) => NewReferralPage(), + comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), + homeHealthCarePage: (context) => HhcProceduresPage(), + organSelectorPage: (context) => OrganSelectorPage(), + symptomsSelectorScreen: (context) => SymptomsSelectorScreen(), + riskFactorsScreen: (context) => RiskFactorsScreen(), + suggestionsScreen: (context) => SuggestionsScreen(), + possibleConditionsScreen: (context) => PossibleConditionsScreen(), + triageScreen: (context) => TriageScreen(), + bloodDonationPage: (context) => BloodDonationPage(), + bookAppointmentPage: (context) => BookAppointmentPage(), + userInfoSelection: (context) => UserInfoSelectionScreen(), + userInfoFlowManager: (context) => UserInfoFlowManager(), + smartWatches: (context) => SmartwatchInstructionsPage(), + huaweiHealthExample: (context) => HuaweiHealthExample(), + covid19Test: (context) => Covid19LandingPage(), + // + waterConsumptionScreen: (context) => WaterConsumptionScreen(), + waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(), + healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + vitalSign: (context) => VitalSignPage() }; } diff --git a/lib/services/analytics/analytics_service.dart b/lib/services/analytics/analytics_service.dart index 3411157..c8f2e48 100644 --- a/lib/services/analytics/analytics_service.dart +++ b/lib/services/analytics/analytics_service.dart @@ -1,8 +1,5 @@ import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:geolocator/geolocator.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/advance_payments.dart'; @@ -15,7 +12,6 @@ import 'package:hmg_patient_app_new/services/analytics/flows/live_care.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/login_registration.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/offers_promotions.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/todo_list.dart'; -import 'package:http/http.dart' as AnalyticEvents; typedef GALogger = Function(String name, {Map parameters}); diff --git a/lib/services/analytics/flows/advance_payments.dart b/lib/services/analytics/flows/advance_payments.dart index 9c8baa1..133e241 100644 --- a/lib/services/analytics/flows/advance_payments.dart +++ b/lib/services/analytics/flows/advance_payments.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class AdvancePayments{ diff --git a/lib/services/analytics/flows/app_nav.dart b/lib/services/analytics/flows/app_nav.dart index bd9186c..75570c6 100644 --- a/lib/services/analytics/flows/app_nav.dart +++ b/lib/services/analytics/flows/app_nav.dart @@ -18,7 +18,7 @@ class AppNav{ if(tabIndex == 3) nav_name = "my family"; if(tabIndex == 4) - nav_name = "todo list"; + nav_name = "todo_section list"; if(tabIndex == 5) nav_name = "help"; diff --git a/lib/services/analytics/flows/appointments.dart b/lib/services/analytics/flows/appointments.dart index 57c45e6..d9991a6 100644 --- a/lib/services/analytics/flows/appointments.dart +++ b/lib/services/analytics/flows/appointments.dart @@ -1,8 +1,5 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; -import 'package:intl/intl.dart'; class Appointment { final GALogger logger; diff --git a/lib/services/analytics/flows/live_care.dart b/lib/services/analytics/flows/live_care.dart index 5e7f330..c5fac58 100644 --- a/lib/services/analytics/flows/live_care.dart +++ b/lib/services/analytics/flows/live_care.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class LiveCare{ diff --git a/lib/services/analytics/flows/login_registration.dart b/lib/services/analytics/flows/login_registration.dart index 9488324..611cc47 100644 --- a/lib/services/analytics/flows/login_registration.dart +++ b/lib/services/analytics/flows/login_registration.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class LoginRegistration{ diff --git a/lib/services/analytics/flows/todo_list.dart b/lib/services/analytics/flows/todo_list.dart index c1c874b..1cc7644 100644 --- a/lib/services/analytics/flows/todo_list.dart +++ b/lib/services/analytics/flows/todo_list.dart @@ -68,7 +68,7 @@ class TodoList{ // to_do_list_confirm_appointment(AppoitmentAllHistoryResultList appointment){ // logger('confirm_appointment', parameters: { // 'appointment_type' : appointment.isLiveCareAppointment! ? 'livecare' : 'regular', - // 'flow_type' : 'todo list', + // 'flow_type' : 'todo_section list', // 'clinic_type_online' : appointment.clinicName, // 'hospital_name' : appointment.projectName, // 'doctor_name' : (appointment.doctorName == null || appointment.doctorName == '') ? appointment.doctorNameObj : appointment.doctorName, diff --git a/lib/services/cache_service.dart b/lib/services/cache_service.dart index 8a015d8..986baa4 100644 --- a/lib/services/cache_service.dart +++ b/lib/services/cache_service.dart @@ -95,7 +95,7 @@ class CacheServiceImp implements CacheService { if (string == null) return null; return json.decode(string); } catch (ex) { - loggerService.errorLogs(ex.toString()); + loggerService.logError(ex.toString()); return null; } } @@ -105,7 +105,7 @@ class CacheServiceImp implements CacheService { try { await sharedPreferences.setString(key, json.encode(value)); } catch (ex) { - loggerService.errorLogs(ex.toString()); + loggerService.logError(ex.toString()); } } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index b91e093..497a009 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -129,13 +129,21 @@ class DialogServiceImp implements DialogService { if (context == null) return; showCommonBottomSheetWithoutHeight(context, title: label ?? "", - child: FamilyCards( - profiles: profiles, - onSelect: (FamilyFileResponseModelLists profile) { - onSwitchPress(profile); - }, - onRemove: (FamilyFileResponseModelLists profile) {}, - isShowDetails: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + (message).toText16(isBold: false, color: AppColors.textColor), + SizedBox(height: 24.h), + FamilyCards( + profiles: profiles, + onSelect: (FamilyFileResponseModelLists profile) { + onSwitchPress(profile); + }, + onRemove: (FamilyFileResponseModelLists profile) {}, + isShowDetails: false, + ), + ], ), callBackFunc: () {}); } diff --git a/lib/services/error_handler_service.dart b/lib/services/error_handler_service.dart index aeefefb..bda1727 100644 --- a/lib/services/error_handler_service.dart +++ b/lib/services/error_handler_service.dart @@ -25,33 +25,33 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { @override Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}) async { if (failure is APIException) { - loggerService.errorLogs("API Exception: ${failure.message}"); + loggerService.logError("API Exception: ${failure.message}"); } else if (failure is ServerFailure) { - loggerService.errorLogs("URL: ${failure.url} \n Server Failure: ${failure.message}"); + loggerService.logError("URL: ${failure.url} \n Server Failure: ${failure.message}"); await _showDialog(failure, title: "Server Failure"); } else if (failure is DataParsingFailure) { - loggerService.errorLogs("Data Parsing Failure: ${failure.message}"); + loggerService.logError("Data Parsing Failure: ${failure.message}"); await _showDialog(failure, title: "Data Error"); } else if (failure is StatusCodeFailure) { - loggerService.errorLogs("StatusCode Failure: ${failure.message}"); + loggerService.logError("StatusCode Failure: ${failure.message}"); await _showDialog(failure, title: "StatusCodeFailure"); } else if (failure is ConnectivityFailure) { - loggerService.errorLogs("ConnectivityFailure : ${failure.message}"); + loggerService.logError("ConnectivityFailure : ${failure.message}"); await _showDialog(failure, title: "ConnectivityFailure ", onOkPressed: () {}); } else if (failure is UnAuthenticatedUserFailure) { - loggerService.errorLogs("URL: ${failure.url} \n UnAuthenticatedUser Failure: ${failure.message}"); + loggerService.logError("URL: ${failure.url} \n UnAuthenticatedUser Failure: ${failure.message}"); await _showDialog(failure, title: "UnAuthenticatedUser Failure", onOkPressed: () => navigationService.replaceAllRoutesAndNavigateToLanding()); } else if (failure is AppUpdateFailure) { - loggerService.errorLogs("AppUpdateFailure : ${failure.message}"); + loggerService.logError("AppUpdateFailure : ${failure.message}"); await _showDialog(failure, title: "AppUpdateFailure Error", onOkPressed: () => navigationService.replaceAllRoutesAndNavigateToLanding()); } else if (failure is HttpException) { - loggerService.errorLogs("Http Exception: ${failure.message}"); + loggerService.logError("Http Exception: ${failure.message}"); await _showDialog(failure, title: "Network Error"); } else if (failure is UnknownFailure) { - loggerService.errorLogs("URL: ${failure.url} \n Unknown Failure: ${failure.message}"); + loggerService.logError("URL: ${failure.url} \n Unknown Failure: ${failure.message}"); await _showDialog(failure, title: "Unknown Failure"); } else if (failure is InvalidCredentials) { - loggerService.errorLogs("Invalid Credentials : ${failure.message}"); + loggerService.logError("Invalid Credentials : ${failure.message}"); await _showDialog(failure, title: "Invalid Credentials "); } else if (failure is UserIntimationFailure) { if (onUnHandledFailure != null) { @@ -66,7 +66,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { await _showDialog(failure, title: "MessageStatusFailure", onOkPressed: onOkPressed); } } else { - loggerService.errorLogs("Unhandled failure type: $failure"); + loggerService.logError("Unhandled failure type: $failure"); await _showDialog(failure, title: "Unhandled Error", onOkPressed: onOkPressed); } } diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart index 9df6633..617539a 100644 --- a/lib/services/logger_service.dart +++ b/lib/services/logger_service.dart @@ -1,7 +1,7 @@ import 'package:logger/logger.dart'; abstract class LoggerService { - void errorLogs(String message); + void logError(String message); void logInfo(String message); } @@ -12,7 +12,7 @@ class LoggerServiceImp implements LoggerService { LoggerServiceImp({required this.logger}); @override - void errorLogs(String message) { + void logError(String message) { logger.e(message); } diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index cb4405c..fe95140 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -13,8 +13,8 @@ class NavigationService { return navigatorKey.currentState!.push(route); } - Future pushAndRemoveUntil(Route route,RoutePredicate predicate) { - return navigatorKey.currentState!.pushAndRemoveUntil(route,predicate); + Future pushAndRemoveUntil(Route route, RoutePredicate predicate) { + return navigatorKey.currentState!.pushAndRemoveUntil(route, predicate); } void pop([T? result]) { @@ -38,11 +38,22 @@ class NavigationService { navigatorKey.currentState?.pushReplacementNamed(routeName); } + void pushPageRoute(String routeName) { + navigatorKey.currentState?.pushNamed(routeName); + } - - Future pushToOtpScreen({required String phoneNumber, required Function(int code) checkActivationCode, required Function(String phoneNumber) onResendOTPPressed, bool isFormFamilyFile = false}) { + Future pushToOtpScreen( + {required String phoneNumber, + required Function(int code) checkActivationCode, + required Function(String phoneNumber) onResendOTPPressed, + bool isFormFamilyFile = false}) { return navigatorKey.currentState!.push( - MaterialPageRoute(builder: (_) => OTPVerificationScreen(phoneNumber: phoneNumber, checkActivationCode: checkActivationCode, onResendOTPPressed: onResendOTPPressed, isFormFamilyFile : isFormFamilyFile)), + MaterialPageRoute( + builder: (_) => OTPVerificationScreen( + phoneNumber: phoneNumber, + checkActivationCode: checkActivationCode, + onResendOTPPressed: onResendOTPPressed, + isFormFamilyFile: isFormFamilyFile)), ); } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 0000000..fd154c6 --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,373 @@ +import 'dart:typed_data'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation; + +/// Abstract class defining the notification service interface +abstract class NotificationService { + /// Initialize the notification service + Future initialize({Function(String payload)? onNotificationClick}); + + /// Request notification permissions (mainly for iOS) + Future requestPermissions(); + + /// Show an immediate notification + Future showNotification({ + required String title, + required String body, + String? payload, + }); + + /// Schedule a notification at a specific date and time + Future scheduleNotification({ + required int id, + required String title, + required String body, + required DateTime scheduledDate, + String? payload, + }); + + /// Schedule daily notifications at specific times + Future scheduleDailyNotifications({ + required List times, + required String title, + required String body, + String? payload, + }); + + /// Schedule water reminder notifications + Future scheduleWaterReminders({ + required List reminderTimes, + required String title, + required String body, + }); + + /// Cancel a specific notification by id + Future cancelNotification(int id); + + /// Cancel all scheduled notifications + Future cancelAllNotifications(); + + /// Get list of pending notifications + Future> getPendingNotifications(); +} + +/// Implementation of NotificationService following the project architecture +class NotificationServiceImp implements NotificationService { + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + final LoggerService loggerService; + + NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); + + // Channel IDs for different notification types + static const String _waterReminderChannelId = 'water_reminder_channel'; + static const String _waterReminderChannelName = 'Water Reminders'; + static const String _waterReminderChannelDescription = 'Daily water intake reminders'; + + static const String _generalChannelId = 'hmg_general_channel'; + static const String _generalChannelName = 'HMG Notifications'; + static const String _generalChannelDescription = 'General notifications from HMG'; + + Function(String payload)? _onNotificationClick; + + @override + Future initialize({Function(String payload)? onNotificationClick}) async { + try { + // Initialize timezone database + tz.initializeTimeZones(); + + // Set local timezone (you can also use a specific timezone if needed) + // For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh')); + final locationName = DateTime.now().timeZoneName; + try { + tz.setLocalLocation(tz.getLocation(locationName)); + } catch (e) { + // Fallback to UTC if specific timezone not found + loggerService.logInfo('Could not set timezone $locationName, using UTC'); + tz.setLocalLocation(tz.getLocation('UTC')); + } + + _onNotificationClick = onNotificationClick; + + const androidSettings = AndroidInitializationSettings('app_icon'); + const iosSettings = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const initializationSettings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: _handleNotificationResponse, + ); + + loggerService.logInfo('NotificationService initialized successfully'); + } catch (ex) { + loggerService.logError('Failed to initialize NotificationService: $ex'); + } + } + + /// Handle notification tap + void _handleNotificationResponse(NotificationResponse response) { + try { + if (response.payload != null && _onNotificationClick != null) { + _onNotificationClick!(response.payload!); + } + loggerService.logInfo('Notification tapped: ${response.payload}'); + } catch (ex) { + loggerService.logError('Error handling notification response: $ex'); + } + } + + @override + Future requestPermissions() async { + try { + // Request permissions for iOS + final result = + await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + + // For Android 13+, permissions are requested at runtime + final androidResult = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.requestNotificationsPermission(); + + loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}'); + return result ?? androidResult ?? true; + } catch (ex) { + loggerService.logError('Error requesting notification permissions: $ex'); + return false; + } + } + + @override + Future showNotification({ + required String title, + required String body, + String? payload, + }) async { + try { + final androidDetails = AndroidNotificationDetails( + _generalChannelId, + _generalChannelName, + channelDescription: _generalChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + ); + + const iosDetails = DarwinNotificationDetails(); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await flutterLocalNotificationsPlugin.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + title, + body, + notificationDetails, + payload: payload, + ); + + loggerService.logInfo('Notification shown: $title'); + } catch (ex) { + loggerService.logError('Error showing notification: $ex'); + } + } + + @override + Future scheduleNotification({ + required int id, + required String title, + required String body, + required DateTime scheduledDate, + String? payload, + }) async { + try { + final androidDetails = AndroidNotificationDetails( + _generalChannelId, + _generalChannelName, + channelDescription: _generalChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + ); + + const iosDetails = DarwinNotificationDetails(); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await flutterLocalNotificationsPlugin.zonedSchedule( + id, + title, + body, + tz.TZDateTime.from(scheduledDate, tz.local), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + payload: payload, + ); + + loggerService.logInfo('Notification scheduled for: $scheduledDate'); + } catch (ex) { + loggerService.logError('Error scheduling notification: $ex'); + } + } + + @override + Future scheduleDailyNotifications({ + required List times, + required String title, + required String body, + String? payload, + }) async { + try { + for (int i = 0; i < times.length; i++) { + final time = times[i]; + await scheduleNotification( + id: i + 1000, + // Offset ID to avoid conflicts + title: title, + body: body, + scheduledDate: time, + payload: payload, + ); + } + + loggerService.logInfo('Scheduled ${times.length} daily notifications'); + } catch (ex) { + loggerService.logError('Error scheduling daily notifications: $ex'); + } + } + + @override + Future scheduleWaterReminders({ + required List reminderTimes, + required String title, + required String body, + }) async { + try { + // Cancel existing water reminders first + await _cancelWaterReminders(); + + final androidDetails = AndroidNotificationDetails( + _waterReminderChannelId, + _waterReminderChannelName, + channelDescription: _waterReminderChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + icon: 'app_icon', + styleInformation: const BigTextStyleInformation(''), + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + for (int i = 0; i < reminderTimes.length; i++) { + final reminderTime = reminderTimes[i]; + final notificationId = 5000 + i; // Use 5000+ range for water reminders + + // Schedule for today if time hasn't passed, otherwise schedule for tomorrow + DateTime scheduledDate = reminderTime; + if (scheduledDate.isBefore(DateTime.now())) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + + await flutterLocalNotificationsPlugin.zonedSchedule( + notificationId, + title, + body, + tz.TZDateTime.from(scheduledDate, tz.local), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time + payload: 'water_reminder_$i', + ); + } + + loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders'); + } catch (ex) { + loggerService.logError('Error scheduling water reminders: $ex'); + } + } + + /// Cancel all water reminders (IDs 5000-5999) + Future _cancelWaterReminders() async { + try { + final pendingNotifications = await getPendingNotifications(); + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + await cancelNotification(notification.id); + } + } + loggerService.logInfo('Cancelled all water reminders'); + } catch (ex) { + loggerService.logError('Error cancelling water reminders: $ex'); + } + } + + @override + Future cancelNotification(int id) async { + try { + await flutterLocalNotificationsPlugin.cancel(id); + loggerService.logInfo('Cancelled notification with ID: $id'); + } catch (ex) { + loggerService.logError('Error cancelling notification: $ex'); + } + } + + @override + Future cancelAllNotifications() async { + try { + await flutterLocalNotificationsPlugin.cancelAll(); + loggerService.logInfo('Cancelled all notifications'); + } catch (ex) { + loggerService.logError('Error cancelling all notifications: $ex'); + } + } + + @override + Future> getPendingNotifications() async { + try { + final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests(); + loggerService.logInfo('Found ${pending.length} pending notifications'); + return pending; + } catch (ex) { + loggerService.logError('Error getting pending notifications: $ex'); + return []; + } + } + + /// Get vibration pattern for notifications + Int64List _getVibrationPattern() { + final vibrationPattern = Int64List(4); + vibrationPattern[0] = 0; + vibrationPattern[1] = 500; + vibrationPattern[2] = 500; + vibrationPattern[3] = 500; + return vibrationPattern; + } +} diff --git a/lib/splashPage.dart b/lib/splashPage.dart index ba7cefd..043eefd 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -1,35 +1,34 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_callkit_incoming/entities/call_event.dart'; +import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; -import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; -import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; - -// import 'package:hmg_patient_app_new/presentation/authantication/login.dart'; -import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; +import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; -import 'package:provider/provider.dart'; import 'core/cache_consts.dart'; -import 'core/utils/local_notifications.dart'; import 'core/utils/push_notification_handler.dart'; -import 'widgets/routes/custom_page_route.dart'; class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + @override _SplashScreenState createState() => _SplashScreenState(); } @@ -38,6 +37,7 @@ class _SplashScreenState extends State { late AuthenticationViewModel authVm; Future initializeStuff() async { + listenerEvent(); Timer( Duration(milliseconds: 500), () async { @@ -47,22 +47,160 @@ class _SplashScreenState extends State { ); await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { - LocalNotification.init(onNotificationClick: (payload) {}); + bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; + + // Initialize NotificationService using dependency injection + final notificationService = getIt.get(); + await notificationService.initialize(onNotificationClick: (payload) { + // Handle notification click here + }); - if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + if (isAppOpenedFromCall) { + navigateToTeleConsult(); } else { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + Navigator.of(context).pushReplacement(FadePage(page: OnboardingScreen())); + } else { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + } } }); var zoom = ZoomVideoSdk(); InitConfig initConfig = InitConfig( domain: "zoom.us", - enableLog: true, + enableLog: false, ); zoom.initSdk(initConfig); } + navigateToTeleConsult() async { + String roomID = await Utils.getStringFromPrefs(CacheConst.zoomRoomID); + print('RoomID: $roomID'); + + // GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); + Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); + + Navigator.of(GetIt.instance().navigatorKey.currentContext!) + .pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.pushReplacementNamed( + // context, + GetIt.instance().navigatorKey.currentContext!, + AppRoutes.zoomCallPage, + // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, 1), + // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + ); + } + + Future listenerEvent() async { + print('Call Canceled : ------->'); + + try { + FlutterCallkitIncoming.onEvent.listen((event) async { + switch (event!.event) { + case Event.actionCallIncoming: + print('actionCallIncoming Event Received: ------->'); + break; + case Event.actionCallStart: + print('actionCallStart Event Received: ------->'); + break; + case Event.actionCallAccept: + print('actionCallAccept Event Received: ------->'); + FlutterCallkitIncoming.endAllCalls(); + try { + navigateToTeleConsult(); + // String roomID = await Utils.getStringFromPrefs(SharedPrefsConsts.ZOOM_ROOM_ID); + // String roomPass = await Utils.getStringFromPrefs(SharedPrefsConsts.ZOOM_ROOM_PASS); + + // print('RoomID: $roomID'); + // print('RoomPass: $roomPass'); + + // Future.delayed(const Duration(milliseconds: 3000), () async { + // // try { + // // String teleConsultURL = "https://teleconsult.meena-health.com/api/videocall/doVideoCall?roomid="; + // // await Utils.getStringFromPrefs(SharedPrefsConsts.PATIENT_MRN).then((value) { + // // teleConsultURL = teleConsultURL + value; + // // print("TeleConsult URL: $teleConsultURL"); + // // Uri uri = Uri.parse(teleConsultURL); + // // launchUrl(uri, mode: LaunchMode.externalApplication, webOnlyWindowName: "TeleConsult"); + // // }); + // + // // var cameraStatus = await Permission.camera.request(); + // // var micStatus = await Permission.microphone.request(); + // // var permissionStatus = await [Permission.microphone, Permission.camera, Permission.notification].request(); + // // if (permissionStatus[1]?.isDenied == true) { + // // await Permission.camera.request(); + // // } + // // if (permissionStatus[0]?.isDenied == true) { + // // await Permission.microphone.request(); + // // } + // + // Navigator.pushNamed( + // // context, + // navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments(roomID, roomPass, "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); + // // } catch (err) { + // // print(err); + // // } + // }); + // String teleConsultURL = "https://teleconsult.meena-health.com/api/videocall/doVideoCall?roomid="; + // await Utils.getStringFromPrefs(SharedPrefsConsts.PATIENT_MRN).then((value) { + // teleConsultURL = teleConsultURL + value; + // print("TeleConsult URL: $teleConsultURL"); + // Uri uri = Uri.parse(teleConsultURL); + // launchUrl(uri, mode: LaunchMode.inAppBrowserView, webOnlyWindowName: "Test"); + // }); + // navigatorKey.currentState!.pushNamed(AppRoutes.incomingCallScreenRoute); + // await Navigator.pushNamed(context, AppRoutes.incomingCallScreenRoute).then((value) {}); + } catch (e) { + print("ERRORRRR!!! ${e.toString()}"); + // await AppSharedPreferences().setObject('call_data', data); + // var data1 = await AppSharedPreferences().getObject('call_data'); + } + break; + case Event.actionCallDecline: + print('actionCallConnected Event Received: ------->'); + await FlutterCallkitIncoming.endAllCalls(); + GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); + break; + case Event.actionCallEnded: + break; + case Event.actionCallTimeout: + break; + case Event.actionCallCallback: + break; + case Event.actionCallToggleHold: + break; + case Event.actionCallToggleMute: + break; + case Event.actionCallToggleDmtf: + break; + case Event.actionCallToggleGroup: + break; + case Event.actionCallToggleAudioSession: + break; + case Event.actionDidUpdateDevicePushTokenVoip: + break; + case Event.actionCallCustom: + break; + case Event.actionCallConnected: + print('actionCallConnected Event Received: ------->'); + await FlutterCallkitIncoming.endAllCalls(); + break; + } + }); + } on Exception catch (e) { + print(e); + print("ERRORRRR!!! ${e.toString()}"); + } + } + /// load the Privilege from service Future loadPrivilege() async { // ProjectViewModel projectProvider = Provider.of(context, listen: false); @@ -78,12 +216,12 @@ class _SplashScreenState extends State { // AppSharedPreferences().setString(APP_LANGUAGE, projectProvider.isArabic ? "ar" : "en"); // var themeNotifier = Provider.of(context, listen: false); // themeNotifier.setTheme(defaultTheme(fontName: projectProvider.isArabic ? 'Cairo' : 'Poppins')); - PushNotificationHandler().init(context); // Asyncronously + // PushNotificationHandler().init(context); // Asyncronously } @override void initState() { - authVm = context.read(); + authVm = getIt(); super.initState(); initializeStuff(); } @@ -92,6 +230,8 @@ class _SplashScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.whiteColor, - body: Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill).center); + body: Lottie.asset(AppAnimations.loadingAnimation, + repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill) + .center); } } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index f185479..c09d537 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -10,7 +10,7 @@ class AppTheme { pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder() }, ), hintColor: Colors.grey[400], diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 6ccf8b1..c631f5b 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -3,21 +3,14 @@ import 'package:flutter/material.dart'; class AppColors { static const transparent = Colors.transparent; static const mainPurple = Color(0xFF7954F7); - static const purpleBg = Color(0xFFAEA4FC); - static const deepPurple = Color(0xFF7C65E7); - static const logoColor = Color(0xFF7C65E7); - static const buttonColor = Color(0xFF6A46F5); - static const splashBgColor = Color(0xFF3C355D); - - static const lightGray = Color(0xFFF4F5F7); - static const lightPurple = Color(0xFFB7A3E6); + static const scaffoldBgColor = Color(0xFFF8F8F8); static const bottomSheetBgColor = Color(0xFFF8F8FA); static const lightGreyEFColor = Color(0xffeaeaff); static const greyF7Color = Color(0xffF7F7F7); + static const greyInfoTextColor = Color(0xff777777); static const lightGrayColor = Color(0xff808080); - static const buttonGrayColor = Color(0xffF1F1F1); - static const lightPurpleAlpha = Color(0x5AB7A3E6); + static const greyTextColorLight = Color(0xFFA2A2A2); // New UI Colors static const whiteColor = Color(0xFFffffff); @@ -37,10 +30,12 @@ class AppColors { static const Color warningColorYellow = Color(0xFFF4A308); static const Color blackBgColor = Color(0xFF2E3039); static const blackColor = textColor; - static const Color inputLabelTextColor = Color(0xff898A8D); + static const Color inputLabelTextColor = Color(0xff898A8D); static const Color greyTextColor = Color(0xFF8F9AA3); static const Color lightGrayBGColor = Color(0x142E3039); + static const Color checkBoxBorderColor = Color(0xffD2D2D2); + static const Color pharmacyBGColor = Color(0xFF359846); static const lightGreenColor = Color(0xFF0ccedde); static const textGreenColor = Color(0xFF18C273); @@ -49,6 +44,7 @@ class AppColors { //Chips static const Color successColor = Color(0xff18C273); + static const Color successLightBgColor = Color(0xffDDF6EA); static const Color errorColor = Color(0xFFED1C2B); static const Color alertColor = Color(0xFFD48D05); static const Color infoColor = Color(0xFF0B85F7); @@ -57,27 +53,60 @@ class AppColors { static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); -static const Color successLightColor = Color(0xFF18C273); -static const Color errorLightColor = Color(0xFFED1C2B); -static const Color alertLightColor = Color(0xFFD48D05); -static const Color infoLightColor = Color(0xFF0B85F7); -static const Color warningLightColor = Color(0xFFFFCC00); -static const Color greyLightColor = Color(0xFFEFEFF0); -static const Color thumbColor = Color(0xFF18C273); -static const Color switchBackgroundColor = Color(0x2618C273); + static const Color successLightColor = Color(0xFF18C273); + static const Color errorLightColor = Color(0xFFED1C2B); + static const Color alertLightColor = Color(0xFFD48D05); + static const Color infoLightColor = Color(0xFF0B85F7); + static const Color warningLightColor = Color(0xFFFFCC00); + static const Color greyLightColor = Color(0xFFEFEFF0); + static const Color thumbColor = Color(0xFF18C273); + static const Color switchBackgroundColor = Color(0x2618C273); + + static const Color bottomNAVBorder = Color(0xFFEEEEEE); + + static const Color quickLoginColor = Color(0xFF666666); + + static const Color tooltipTextColor = Color(0xFF414D55); + static const Color graphGridColor = Color(0x4D18C273); + static const Color criticalLowAndHigh = Color(0xFFED1C2B); + static const Color highAndLow = Color(0xFFFFAF15); + static const Color labelTextColor = Color(0xFF838383); + static const Color calenderTextColor = Color(0xFFD0D0D0); + static const Color lightGreenButtonColor = Color(0x2618C273); + + static const Color lightRedButtonColor = Color(0x1AED1C2B); + + // Status Colors + static const Color statusPendingColor = Color(0xffCC9B14); + static const Color statusProcessingColor = Color(0xff2E303A); + static const Color statusCompletedColor = Color(0xff359846); + static const Color statusRejectedColor = Color(0xffD02127); + + // Info Banner Colors + static const Color infoBannerBgColor = Color(0xFFFFF4E6); + static const Color infoBannerBorderColor = Color(0xFFFFE5B4); + static const Color infoBannerIconColor = Color(0xFFCC9B14); + static const Color infoBannerTextColor = Color(0xFF856404); -static const Color bottomNAVBorder = Color(0xFFEEEEEE); + // SymptomsChecker -static const Color quickLoginColor = Color(0xFF666666); + static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); + static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); + static const Color chipColorMonitor = Color(0xFF18C273); + static const Color chipColorEmergency = Color(0xFFED1C2B); -static const Color tooltipTextColor = Color(0xFF414D55); -static const Color graphGridColor = Color(0x4D18C273); -static const Color criticalLowAndHigh = Color(0xFFED1C2B); -static const Color highAndLow = Color(0xFFFFAF15); -static const Color labelTextColor = Color(0xFF838383); -static const Color calenderTextColor = Color(0xFFD0D0D0); -static const Color lightGreenButtonColor = Color(0x2618C273); + // Services Page Colors + static const Color eReferralCardColor = Color(0xFFFF8012); + static const Color bloodDonationCardColor = Color(0xFFFF5662); + static const Color bookAppointment = Color(0xFF415364); -static const Color lightRedButtonColor = Color(0x1AED1C2B); + // Water Monitor + static const Color blueColor = Color(0xFF4EB5FF); + static const Color blueGradientColorOne = Color(0xFFF1F7FD); + static const Color blueGradientColorTwo = Color(0xFFD9EFFF); + // Shimmer + static const Color shimmerBaseColor = Color(0xFFE0E0E0); + static const Color shimmerHighlightColor = Color(0xFFF5F5F5); + static const Color covid29Color = Color(0xff2563EB); } diff --git a/lib/widgets/CustomSwitch.dart b/lib/widgets/CustomSwitch.dart index 784e446..bca4e69 100644 --- a/lib/widgets/CustomSwitch.dart +++ b/lib/widgets/CustomSwitch.dart @@ -21,7 +21,7 @@ class _CustomSwitchState extends State { width: 48.w, height: 30.h, decoration: BoxDecoration( - color: AppColors.switchBackgroundColor , + color: widget.value ? AppColors.switchBackgroundColor : AppColors.greyTextColor, borderRadius: BorderRadius.circular(18), ), child: AnimatedAlign( @@ -32,7 +32,7 @@ class _CustomSwitchState extends State { width: 28.w, height: 28.h, decoration: BoxDecoration( - color: AppColors.thumbColor, + color: widget.value? AppColors.thumbColor : AppColors.greyColor, shape: BoxShape.circle, ), ), diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index f6db2f4..5409fcf 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,6 +5,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -21,10 +21,13 @@ class CollapsingListView extends StatelessWidget { VoidCallback? history; VoidCallback? instructions; VoidCallback? requests; + VoidCallback? sendEmail; Widget? bottomChild; Widget? trailing; bool isClose; bool isLeading; + VoidCallback? leadingCallback; + ScrollPhysics? physics; CollapsingListView({ super.key, @@ -38,10 +41,15 @@ class CollapsingListView extends StatelessWidget { this.history, this.instructions, this.requests, + this.sendEmail, this.isLeading = true, this.trailing, + this.leadingCallback, + this.physics, }); + final ScrollController _controller = ScrollController(); + @override Widget build(BuildContext context) { AppState appState = getIt.get(); @@ -50,87 +58,63 @@ class CollapsingListView extends StatelessWidget { body: Column( children: [ CustomScrollView( + physics: physics, + controller: _controller, slivers: [ SliverAppBar( - automaticallyImplyLeading: false, + automaticallyImplyLeading: isLeading, pinned: true, - expandedHeight: 100.h, - stretch: true, + // toolbarHeight: isLeading ? 24.h : kToolbarHeight, + leadingWidth: isLeading ? null : double.infinity, systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), surfaceTintColor: Colors.transparent, backgroundColor: AppColors.bgScaffoldColor, + bottom: isLeading + ? ScrollAnimatedTitle( + title: title, + showBack: true, + controller: _controller, + search: search, + report: report, + logout: logout, + history: history, + instructions: instructions, + requests: requests, + sendEmail: sendEmail, + bottomChild: bottomChild, + trailing: trailing, + ) + : null, leading: isLeading ? Transform.flip( flipX: appState.isArabic(), child: IconButton( - icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), + icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 24.h, height: 24.h), padding: EdgeInsets.only(left: 12), - onPressed: () => Navigator.pop(context), + onPressed: () { + if (leadingCallback != null) { + leadingCallback!(); + } else { + context.pop(); + } + }, highlightColor: Colors.transparent, ), ) - : SizedBox.shrink(), - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - final double maxHeight = 100.h; - final double minHeight = kToolbarHeight; - double t = (constraints.maxHeight - minHeight) / (maxHeight - minHeight); - t = t - 1; - if (t < 0.7) t = 0.7; - t = t.clamp(0.0, 1.0); - - final double fontSize = lerpDouble(14, 18, t)!; - final double bottomPadding = lerpDouble(0, 0, t)!; - final double leftPadding = lerpDouble(150, 24, t)!; - - return Stack( - children: [ - Align( - alignment: Alignment.lerp( - Alignment.center, - Alignment.bottomLeft, - t, - )!, - child: Padding( - padding: EdgeInsets.only( - left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding), - child: Row( - spacing: 4.h, - children: [ - Text( - title, - maxLines: 1, - style: TextStyle( - fontSize: (27 - (5 * (2 - t))).f, - fontWeight: FontWeight.lerp( - FontWeight.w300, - FontWeight.w600, - t, - )!, - color: AppColors.blackColor, - letterSpacing: -0.5), - ).expanded, - if (logout != null) - actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!), - if (report != null) - actionButton(context, t, title: "Report".needTranslation, icon: AppAssets.report_icon).onPress(report!), - if (history != null) - actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon) - .onPress(history!), - if (instructions != null) - actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!), - if (requests != null) - actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon) - .onPress(requests!), - if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24), - if (trailing != null) trailing!, - ], - )), - ), - ], - ); - }, - ), + : ScrollAnimatedTitle( + title: title, + showBack: false, + controller: _controller, + search: search, + report: report, + logout: logout, + history: history, + instructions: instructions, + requests: requests, + sendEmail: sendEmail, + bottomChild: bottomChild, + trailing: trailing, + ), ), SliverList( delegate: SliverChildBuilderDelegate( @@ -146,13 +130,165 @@ class CollapsingListView extends StatelessWidget { ); } +// Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { +// return AnimatedSize( +// duration: Duration(milliseconds: 150), +// child: Container( +// height: 40.h, +// padding: EdgeInsets.all(8.w), +// margin: EdgeInsets.only(right: 24.w), +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration( +// color: AppColors.secondaryLightRedColor, +// borderRadius: 10.r, +// ), +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.center, +// mainAxisAlignment: MainAxisAlignment.center, +// mainAxisSize: MainAxisSize.min, +// spacing: 8.h, +// children: [ +// Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), +// if (t == 1) +// Text( +// title, +// style: context.dynamicTextStyle( +// color: AppColors.primaryRedColor, +// letterSpacing: -0.4, +// fontSize: (12 - (2 * (1 - t))).f, +// fontWeight: FontWeight.lerp( +// FontWeight.w300, +// FontWeight.w500, +// t, +// )!, +// ), +// ), +// ], +// ), +// ), +// ); +// } +} +// +// class ActionModel { +// bool requireT; +// +// } + +class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget { + final String title; + final bool showBack; + final ScrollController controller; + + VoidCallback? search; + VoidCallback? report; + VoidCallback? logout; + VoidCallback? history; + VoidCallback? instructions; + VoidCallback? requests; + VoidCallback? sendEmail; + Widget? bottomChild; + Widget? trailing; + + ScrollAnimatedTitle({ + super.key, + required this.title, + required this.controller, + required this.showBack, + this.search, + this.report, + this.logout, + this.history, + this.instructions, + this.requests, + this.sendEmail, + this.bottomChild, + this.trailing, + }); + + @override + Size get preferredSize => const Size.fromHeight(50); + + @override + State createState() => _ScrollAnimatedTitleState(); +} + +class _ScrollAnimatedTitleState extends State { + static const double _maxFont = 26; + static const double _minFont = 18; + + static const double _maxHeight = 80; + static const double _minHeight = 56; + + double _fontSize = _maxFont; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onScroll); + } + + @override + void dispose() { + widget.controller.removeListener(_onScroll); + super.dispose(); + } + + double t = 0; + + void _onScroll() { + final double offset = widget.controller.offset; + + // control animation range + const double range = 120; + + final double t = (1 - (offset / range)).clamp(0.0, 1.0); + this.t = t; + setState(() { + _fontSize = _minFont + (_maxFont - _minFont) * t; + }); + } + + @override + Widget build(BuildContext context) { + final isRtl = Directionality.of(context) == TextDirection.rtl; + return Container( + height: (widget.preferredSize.height - _fontSize / 2).h, + alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), + padding: EdgeInsets.fromLTRB(24, 0, 24, 0), + child: Row( + spacing: 4.h, + children: [ + Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: _fontSize, + fontWeight: FontWeight.bold, + letterSpacing: -1.0, + ), + ).expanded, + ...[ + if (widget.logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.sendEmail != null) actionButton(context, t, title: "Send Email".needTranslation, icon: AppAssets.email).onPress(widget.sendEmail!), + if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!), + if (widget.trailing != null) widget.trailing!, + ] + ], + ), + ); + } + Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { return AnimatedSize( duration: Duration(milliseconds: 150), child: Container( - height: 40.h, + height: 36.h + (4.h * t), padding: EdgeInsets.all(8.w), - margin: EdgeInsets.only(right: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.secondaryLightRedColor, borderRadius: 10.r, @@ -164,15 +300,15 @@ class CollapsingListView extends StatelessWidget { spacing: 8.h, children: [ Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), - if (t == 1) + if (t >= .5) Text( title, style: context.dynamicTextStyle( color: AppColors.primaryRedColor, letterSpacing: -0.4, - fontSize: (14 - (2 * (1 - t))).f, + fontSize: (12 - (2 * (1 - t))).f, fontWeight: FontWeight.lerp( - FontWeight.w300, + FontWeight.w400, FontWeight.w500, t, )!, diff --git a/lib/widgets/appbar/collapsing_toolbar.dart b/lib/widgets/appbar/collapsing_toolbar.dart new file mode 100644 index 0000000..87cf15a --- /dev/null +++ b/lib/widgets/appbar/collapsing_toolbar.dart @@ -0,0 +1,205 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +import '../../core/dependencies.dart'; + +class CollapsingToolbar extends StatefulWidget { + final String title; + Widget child; + VoidCallback? search; + VoidCallback? report; + VoidCallback? logout; + VoidCallback? history; + VoidCallback? instructions; + VoidCallback? requests; + Widget? bottomChild; + Widget? trailing; + bool isClose; + bool isLeading; + + CollapsingToolbar({ + super.key, + required this.title, + required this.child, + this.search, + this.isClose = false, + this.bottomChild, + this.report, + this.logout, + this.history, + this.instructions, + this.requests, + this.isLeading = true, + this.trailing, + }); + + @override + State createState() => _CollapsingToolbarState(); +} + +class _CollapsingToolbarState extends State { + bool isCollapsed = false; + + final ScrollController _controller = ScrollController(); + double expandedHeight = 0; + + double get maxCollapseOffset => expandedHeight - kToolbarHeight; + @override + void initState() { + super.initState(); + + + _controller.addListener(() { + // If scrolling UP beyond collapsed point → force stop + print("the height is $maxCollapseOffset"); + if (_controller.offset > maxCollapseOffset) { + _controller.jumpTo(maxCollapseOffset); + } + }); + } + + @override + Widget build(BuildContext context) { + expandedHeight = MediaQuery.of(context).size.height * 0.11.h; + AppState appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: + // Column( + // children: [ + NestedScrollView( + controller: _controller, + floatHeaderSlivers: true, + physics: isCollapsed?NeverScrollableScrollPhysics():BouncingScrollPhysics(), + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverAppBar( + automaticallyImplyLeading: false, + pinned: true, + expandedHeight: MediaQuery.of(context).size.height * 0.11.h, + stretch: true, + systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), + surfaceTintColor: Colors.transparent, + backgroundColor: AppColors.bgScaffoldColor, + leading: widget.isLeading + ? Transform.flip( + flipX: appState.isArabic(), + child: IconButton( + icon: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), + padding: EdgeInsets.only(left: 12), + onPressed: () => Navigator.pop(context), + highlightColor: Colors.transparent, + ), + ) + : SizedBox.shrink(), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final double maxHeight = 100.h; + final double minHeight = kToolbarHeight; + double t = (constraints.maxHeight - minHeight) / (maxHeight - minHeight); + t = t - 1; + if (t < 0.7) t = 0.7; + t = t.clamp(0.0, 1.0); + + final double fontSize = lerpDouble(14, 18, t)!; + final double bottomPadding = lerpDouble(0, 0, t)!; + final double leftPadding = lerpDouble(150, 24, t)!; + + return Stack( + children: [ + Align( + alignment: Alignment.lerp( + Alignment.center, + Alignment.bottomLeft, + t, + )!, + child: Padding( + padding: EdgeInsets.only(left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding), + child: Row( + spacing: 4.h, + children: [ + Text( + widget.title, + maxLines: 1, + style: TextStyle( + fontSize: (27 - (5 * (2 - t))).f, + fontWeight: FontWeight.lerp( + FontWeight.w300, + FontWeight.w600, + t, + )!, + color: AppColors.blackColor, + letterSpacing: -0.5), + ).expanded, + if (widget.logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: "Report".needTranslation, icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!).paddingOnly(right: 24), + if (widget.trailing != null) widget.trailing!, + ], + )), + ), + ], + ); + }, + ), + ), + ]; + }, + body: widget.child, + ), + + // ], + // ), + ); + } + + Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { + return AnimatedSize( + duration: Duration(milliseconds: 150), + child: Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + margin: EdgeInsets.only(right: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.secondaryLightRedColor, + borderRadius: 10.r, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 8.h, + children: [ + Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), + if (t == 1) + Text( + title, + style: context.dynamicTextStyle( + color: AppColors.primaryRedColor, + letterSpacing: -0.4, + fontSize: (14 - (2 * (1 - t))).f, + fontWeight: FontWeight.lerp( + FontWeight.w300, + FontWeight.w500, + t, + )!, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index 64c2485..be443f8 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -45,7 +45,7 @@ class AttachmentOptions extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SvgPicture.asset( - "assets/images/$icon", + "assets/images/svg/$icon", ), title.toText11(isBold: true), ], diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index a3e117f..2da1d44 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -21,7 +21,7 @@ class BottomNavigation extends StatelessWidget { final items = [ BottomNavItem(icon: AppAssets.homeBottom, label: LocaleKeys.home.tr(context: context)), appState.isAuthenticated - ? BottomNavItem(icon: AppAssets.myFilesBottom, label: LocaleKeys.myFiles.tr(context: context)) + ? BottomNavItem(icon: AppAssets.myFilesBottom, label: LocaleKeys.medicalFile.tr(context: context)) : BottomNavItem(icon: AppAssets.feedback, label: LocaleKeys.feedback.tr()), BottomNavItem( icon: AppAssets.bookAppoBottom, @@ -30,7 +30,8 @@ class BottomNavigation extends StatelessWidget { isSpecial: true, ), appState.isAuthenticated - ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) + // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) + ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, label: "Symptoms") : BottomNavItem(icon: AppAssets.news, label: LocaleKeys.news.tr()), BottomNavItem(icon: AppAssets.servicesBottom, label: LocaleKeys.services2.tr(context: context)), ]; @@ -39,7 +40,7 @@ class BottomNavigation extends StatelessWidget { decoration: _containerDecoration, padding: _containerPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( items.length, (index) => _buildNavItem(items[index], index), @@ -61,12 +62,12 @@ class BottomNavigation extends StatelessWidget { child: Utils.buildSvgWithAssets( icon: item.icon, height: item.iconSize.h, - width: item.iconSize.h, + width: item.iconSize.w, // iconColor: isSelected ? Colors.black87 : Colors.black87, ), ), const SizedBox(height: 10), - item.label.toText12(fontWeight: FontWeight.w500), + item.label.toText11(weight: FontWeight.w500), SizedBox(height: item.isSpecial ? 5 : 0) ], ), @@ -84,7 +85,7 @@ class BottomNavItem { const BottomNavItem({ required this.icon, required this.label, - this.iconSize = 21, + this.iconSize = 24, this.isSpecial = false, this.color, }); diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index f236bd9..08d281f 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class CustomButton extends StatelessWidget { final String text; @@ -64,17 +65,17 @@ class CustomButton extends StatelessWidget { width: width, padding: padding, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isDisabled ? backgroundColor.withOpacity(.5) : backgroundColor, + color: isDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor, borderRadius: radius, customBorder: BorderRadius.circular(radius), - side: borderSide ?? BorderSide(width: borderWidth.h, color: isDisabled ? borderColor.withValues(alpha: 0.5) : borderColor)), + side: borderSide ?? BorderSide(width: borderWidth.h, color: isDisabled ? Colors.transparent : borderColor)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 8.h, left: 8.h) : EdgeInsets.zero, + padding: text.isNotEmpty ? EdgeInsets.only(right: 8.w, left: 8.w) : EdgeInsets.zero, child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconS, height: iconS), ), Visibility( @@ -86,7 +87,7 @@ class CustomButton extends StatelessWidget { overflow: textOverflow, style: context.dynamicTextStyle( fontSize: fontS, - color: isDisabled ? textColor.withOpacity(0.5) : textColor, + color: isDisabled ? AppColors.greyTextColor : textColor, letterSpacing: 0, fontWeight: fontWeight, ), diff --git a/lib/widgets/buttons/default_button.dart b/lib/widgets/buttons/default_button.dart index eb02c38..d8d8cac 100644 --- a/lib/widgets/buttons/default_button.dart +++ b/lib/widgets/buttons/default_button.dart @@ -1,7 +1,6 @@ -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; extension WithContainer on Widget { Widget get insideContainer => Container( @@ -26,20 +25,22 @@ class DefaultButton extends StatelessWidget { final double height; final double borderRadius; - const DefaultButton(this.text, this.onPress, - {Key? key, - this.color, - this.isTextExpanded = true, - this.svgIcon, - this.disabledColor, - this.count = 0, - this.textColor = Colors.white, - this.iconData, - this.fontSize, - this.colors, - this.height = 50, - this.borderRadius = 100}) - : super(key: key); + const DefaultButton( + this.text, + this.onPress, { + super.key, + this.color, + this.isTextExpanded = true, + this.svgIcon, + this.disabledColor, + this.count = 0, + this.textColor = Colors.white, + this.iconData, + this.fontSize, + this.colors, + this.height = 50, + this.borderRadius = 100, + }); @override Widget build(BuildContext context) { @@ -48,30 +49,20 @@ class DefaultButton extends StatelessWidget { child: Container( height: height, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(borderRadius), - gradient: onPress == null - ? LinearGradient( - colors: [ - disabledColor ?? const Color(0xffEAEAEA), - disabledColor ?? const Color(0xffEAEAEA), - ], - ) - : LinearGradient( - transform: const GradientRotation(.83), - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: colors ?? - [ - AppColors.buttonColor, - AppColors.buttonColor, - ], - ), - ), + borderRadius: BorderRadius.circular(borderRadius), + gradient: onPress == null + ? LinearGradient( + colors: [ + disabledColor ?? const Color(0xffEAEAEA), + disabledColor ?? const Color(0xffEAEAEA), + ], + ) + : null), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (iconData != null) Icon(iconData, color: textColor), - if (svgIcon != null) SvgPicture.asset(svgIcon ?? "", color: textColor), + if (svgIcon != null) SvgPicture.asset(svgIcon ?? ""), if (!isTextExpanded) Padding( padding: EdgeInsets.only( diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index f090f4f..8597b3e 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -22,8 +22,10 @@ class AppCustomChipWidget extends StatelessWidget { this.deleteIconColor = AppColors.textColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, + this.isIconPNG = false, this.onChipTap, this.labelPadding, + this.onDeleteTap, }); final String? labelText; @@ -41,78 +43,86 @@ class AppCustomChipWidget extends StatelessWidget { final OutlinedBorder? shape; final EdgeInsets? padding; final EdgeInsetsDirectional? labelPadding; + final bool isIconPNG; final void Function()? onChipTap; + final void Function()? onDeleteTap; @override Widget build(BuildContext context) { final iconS = iconSize ?? 12.w; return GestureDetector( onTap: onChipTap, - child: ChipTheme( - data: ChipThemeData( - padding: EdgeInsets.zero, - shape: SmoothRectangleBorder( - side: BorderSide( - width: 10.w, - color: Colors.transparent, // Crucially, set color to transparent - style: BorderStyle.none, + child: SizedBox( + child: ChipTheme( + data: ChipThemeData( + padding: EdgeInsets.zero, + shape: SmoothRectangleBorder( + side: BorderSide( + width: 10.w, + color: Colors.transparent, // Crucially, set color to transparent + style: BorderStyle.none, + ), + borderRadius: BorderRadius.circular(8.r), // Apply a border radius of 16.0 ), - borderRadius: BorderRadius.circular(isFoldable || isTablet ? 6.r : 8.r), ), + child: icon.isNotEmpty + ? Chip( + avatar: icon.isNotEmpty + ? isIconPNG + ? Image.asset(icon, width: iconS, height: iconS) + : Utils.buildSvgWithAssets( + icon: icon, + width: iconS, + height: iconS, + iconColor: iconHasColor ? iconColor : null, + fit: BoxFit.contain, + ) + : SizedBox.shrink(), + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), + padding: padding, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), + backgroundColor: backgroundColor, + shape: shape ?? + SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + smoothness: 10, + side: BorderSide(color: AppColors.transparent, width: 1.5), + ), + deleteIcon: deleteIcon?.isNotEmpty == true + ? InkWell( + onTap: onDeleteTap, + child: Utils.buildSvgWithAssets( + icon: deleteIcon!, + width: iconS, + height: iconS, + iconColor: deleteIconHasColor ? deleteIconColor : null, + ), + ) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, + ) + : Chip( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor, isCenter: true), + padding: EdgeInsets.zero, + backgroundColor: backgroundColor, + shape: shape ?? + SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + smoothness: 10, + side: BorderSide(color: AppColors.transparent, width: 1.5), + ), + labelPadding: labelPadding ?? EdgeInsetsDirectional.only(start: 6.w, end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), + deleteIcon: deleteIcon?.isNotEmpty == true + ? InkWell( + onTap: onDeleteTap, + child: Utils.buildSvgWithAssets( + icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? deleteIconColor : null)) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, + ), ), - child: icon.isNotEmpty - ? Chip( - avatar: icon.isNotEmpty - ? Padding( - padding: EdgeInsets.only(left: 8.w, right: 6.w), - child: Utils.buildSvgWithAssets( - icon: icon, - width: iconS, - height: iconS, - iconColor: iconHasColor ? iconColor : null, - fit: BoxFit.contain, - ), - ) - : SizedBox.shrink(), - avatarBoxConstraints: BoxConstraints(), - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: EdgeInsetsDirectional.only(end: 8.w), - backgroundColor: backgroundColor, - shape: shape ?? - SmoothRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - smoothness: 10, - side: BorderSide(color: AppColors.transparent, width: 1.5), - ), - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets( - icon: deleteIcon!, - width: iconS, - height: iconS, - iconColor: deleteIconHasColor ? deleteIconColor : null, - ) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ) - : Chip( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), - padding: EdgeInsets.zero, - backgroundColor: backgroundColor, - shape: shape ?? - SmoothRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - smoothness: 10, - side: BorderSide(color: AppColors.transparent, width: 1.5), - ), - labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets(icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? deleteIconColor : null) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ), ), ); } diff --git a/lib/widgets/chip/custom_chip_widget.dart b/lib/widgets/chip/custom_chip_widget.dart index 5dee4f0..b60db57 100644 --- a/lib/widgets/chip/custom_chip_widget.dart +++ b/lib/widgets/chip/custom_chip_widget.dart @@ -67,7 +67,12 @@ class CustomChipWidget extends StatelessWidget { Utils.buildSvgWithAssets(icon: iconAsset!), SizedBox(width: 4.h), ], - chipText.toText10(isBold: true, color: isSelected ? Colors.white : textColor ?? chipType.color, maxlines: 1, weight: FontWeight.w500, letterSpacing: -0.5), + chipText.toText10( + isBold: true, + color: isSelected ? Colors.white : textColor ?? chipType.color, + maxlines: 1, + weight: FontWeight.w500, + letterSpacing: -0.5), ], ), ), diff --git a/lib/widgets/chip/custom_selectable_chip.dart b/lib/widgets/chip/custom_selectable_chip.dart new file mode 100644 index 0000000..be42bfd --- /dev/null +++ b/lib/widgets/chip/custom_selectable_chip.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class CustomSelectableChip extends StatelessWidget { + final String label; + final bool selected; + final VoidCallback? onTap; + final Color activeColor; + final Color activeTextColor; + final Color inactiveBorderColor; + final Color inactiveTextColor; + + const CustomSelectableChip({ + super.key, + required this.label, + required this.selected, + this.onTap, + this.activeColor = const Color(0xFFD03C32), // red accent + this.activeTextColor = Colors.white, + this.inactiveBorderColor = const Color(0xFFE8E8E8), + this.inactiveTextColor = const Color(0xFF222222), + }); + + @override + Widget build(BuildContext context) { + final double radius = 8.0; + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: selected ? activeColor.withValues(alpha: 0.12) : AppColors.whiteColor, + borderRadius: BorderRadius.circular(radius), + border: Border.all( + color: selected ? activeColor : inactiveBorderColor, + width: 1, + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(radius), + onTap: onTap, + child: Text( + label, + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: selected ? activeTextColor : inactiveTextColor, + letterSpacing: -0.02 * 18, + height: 1.0, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 3aaefe7..6ff5cc5 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -1,11 +1,65 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_reminder_view.dart'; +import 'package:hmg_patient_app_new/services/permission_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:permission_handler/permission_handler.dart'; + + +class BottomSheetUtils{ + showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { + if (Platform.isAndroid) { + if (await PermissionService.isCalendarPermissionEnabled()) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } else { + // Utils.showPermissionConsentDialog(context, TranslationBase.of(context).calendarPermission, () async { + // if (await Permission.calendarFullAccess.request().isGranted) { + // _showReminderDialog(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + // onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + // } + // }); + } + } else { + if (await Permission.calendarWriteOnly.request().isGranted) { + if (await Permission.calendarFullAccess.request().isGranted) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } + } + } + } + Future _showReminderBottomSheet(BuildContext providedContext, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async { + showCommonBottomSheetWithoutHeight(providedContext, title: "Set the timer of reminder".needTranslation, child: PrescriptionReminderView( + setReminder: (int value) async { + if (!isMultiAllowed!) { + if (onMultiDateSuccess == null) { + CalenderUtilsNew calendarUtils = CalenderUtilsNew.instance; + await calendarUtils.createOrUpdateEvent( + title: title ?? "You have appointment with Dr. ".needTranslation + doctorName, + description: description ?? "At " + appoDateFormatted + " " + appoTimeFormatted, + scheduleDateTime: dateTime, + eventId: eventId, + location: ''); + onSuccess(); + } + } else { + onMultiDateSuccess!(value); + } + }, + ), callBackFunc: () {}, isFullScreen: false); + } +} void showCommonBottomSheet(BuildContext context, {required Widget child, Function(String?)? callBackFunc, @@ -54,11 +108,11 @@ class ButtonSheetContent extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // SizedBox( - // height: 20.h, - // ), + SizedBox( + height: 20.h, + ), // Center( // child: Container( // margin: const EdgeInsets.only(top: 18, bottom: 12), @@ -73,10 +127,9 @@ class ButtonSheetContent extends StatelessWidget { // Close button isCloseButtonVisible && isFullScreen - ? Column(children: [ - SizedBox( - height: 40.h, - ), + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ Padding( padding: EdgeInsets.symmetric( horizontal: 16, @@ -91,7 +144,7 @@ class ButtonSheetContent extends StatelessWidget { isFullScreen ? Column( children: [ - SizedBox(height: 20.h), + // SizedBox(height: 20.h), Padding(padding: EdgeInsets.symmetric(horizontal: 16.h), child: title.toText24(isBold: true)), SizedBox(height: 16.h), ], @@ -105,18 +158,19 @@ class ButtonSheetContent extends StatelessWidget { } void showCommonBottomSheetWithoutHeight( - BuildContext context, { - required Widget child, - required VoidCallback callBackFunc, - String title = "", - bool isCloseButtonVisible = true, - bool isFullScreen = true, - bool isDismissible = true, - Widget? titleWidget, - bool useSafeArea = false, + BuildContext context, { + required Widget child, + VoidCallback? callBackFunc, + String title = "", + bool isCloseButtonVisible = true, + bool isFullScreen = true, + bool isDismissible = true, + Widget? titleWidget, + bool useSafeArea = false, bool hasBottomPadding = true, + EdgeInsets? padding, Color backgroundColor = AppColors.bottomSheetBgColor, - VoidCallback? onCloseClicked + VoidCallback? onCloseClicked, }) { showModalBottomSheet( sheetAnimationStyle: AnimationStyle( @@ -124,7 +178,8 @@ void showCommonBottomSheetWithoutHeight( reverseDuration: Duration(milliseconds: 300), ), constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width, // Full width + + maxWidth: MediaQuery.sizeOf(context).width//MediaQuery.of(context).size.width, // Full width ), context: context, isScrollControlled: true, @@ -146,7 +201,7 @@ void showCommonBottomSheetWithoutHeight( physics: ClampingScrollPhysics(), child: isCloseButtonVisible ? Container( - padding: EdgeInsets.only( + padding: padding ?? const EdgeInsets.only( left: 24, top: 24, right: 24, @@ -158,25 +213,30 @@ void showCommonBottomSheetWithoutHeight( ), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - titleWidget ?? - Expanded( - child: title.toText20(weight: FontWeight.w600), - ), - Utils.buildSvgWithAssets( - icon: AppAssets.close_bottom_sheet_icon, - iconColor: Color(0xff2B353E), - ).onPress(() { - onCloseClicked?.call(); - Navigator.of(context).pop(); - }), - ], + Padding( + padding: padding != null? EdgeInsets.symmetric(horizontal: 24.w): EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + titleWidget ?? + Expanded( + child: title.toText20(weight: FontWeight.w600), + ), + if (isCloseButtonVisible) ...[ + Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + iconColor: Color(0xff2B353E), + ).onPress(() { + onCloseClicked?.call(); + Navigator.of(context).pop(); + }),], + ], + ), ), - SizedBox(height: 16.h), + isCloseButtonVisible ? SizedBox(height: 16.h) : SizedBox.shrink(), child, ], ), @@ -187,7 +247,9 @@ void showCommonBottomSheetWithoutHeight( ); }, ).then((value) { - callBackFunc(); + if (callBackFunc != null) { + callBackFunc(); + } }); } diff --git a/lib/widgets/countdown_timer.dart b/lib/widgets/countdown_timer.dart new file mode 100644 index 0000000..165a833 --- /dev/null +++ b/lib/widgets/countdown_timer.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; + +Widget buildTime(Duration duration, {bool isHomePage = false}) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = twoDigits(duration.inHours); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildTimeColumn(hours, "Hours".needTranslation), + buildTimeColumn(minutes, "Mins".needTranslation), + buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), + ], + ); +} + +Widget buildTimeColumn(String time, String label, {bool isLast = false}) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + buildDigit(time[0]), + buildDigit(time[1]), + if (!isLast) buildTimeSeparator(), + ], + ), + buildLabel(label), + ], + ); +} + +Widget buildDigit(String digit) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + // margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: ClipRect( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + switchInCurve: Curves.easeOutExpo, + switchOutCurve: Curves.easeInExpo, + transitionBuilder: (Widget child, Animation animation) { + return Stack( + children: [ + SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: const Offset(0, 1), + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + )), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.bounceIn, + )), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ], + ); + }, + child: Text( + digit, + key: ValueKey(digit), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + fontSize: 14.f, + ), + ), + ), + ), + ); +} + +Widget buildLabel(String label) { + return label.toText12(isBold: true); +} + +Widget buildTimeSeparator() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 2.0), + child: Text( + ":", + style: TextStyle( + color: Colors.black, + fontSize: 20, + ), + ), + ); +} diff --git a/lib/widgets/custom_tab_bar.dart b/lib/widgets/custom_tab_bar.dart index c65cd75..c30c070 100644 --- a/lib/widgets/custom_tab_bar.dart +++ b/lib/widgets/custom_tab_bar.dart @@ -14,7 +14,7 @@ class CustomTabBarModel { } class CustomTabBar extends StatefulWidget { - final int initialIndex = 0; + final int initialIndex; final List tabs; final Color activeTextColor; final Color activeBackgroundColor; @@ -25,6 +25,7 @@ class CustomTabBar extends StatefulWidget { const CustomTabBar({ super.key, required this.tabs, + this.initialIndex = 0, this.activeTextColor = const Color(0xff2E3039), this.inActiveTextColor = const Color(0xff898A8D), this.activeBackgroundColor = const Color(0x142E3039), @@ -41,6 +42,7 @@ class CustomTabBarState extends State { @override void initState() { + selectedIndex = widget.initialIndex; super.initState(); } @@ -59,6 +61,7 @@ class CustomTabBarState extends State { if (widget.tabs.length > 3) { parentWidget = ListView.separated( + shrinkWrap: true, scrollDirection: Axis.horizontal, padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), @@ -75,12 +78,12 @@ class CustomTabBarState extends State { } return Container( + height: 62.h, padding: EdgeInsets.all(4.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 12.r, + borderRadius: 10.r, ), - height: 62.h, child: Center(child: parentWidget)); } @@ -92,7 +95,7 @@ class CustomTabBarState extends State { alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: isSelected ? widget.activeBackgroundColor : widget.inActiveBackgroundColor, - borderRadius: 12.r, + borderRadius: 10.r, ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 8620ab5..debc069 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -14,7 +14,6 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; -import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; typedef OnRangeSelected = void Function(DateTime? start, DateTime? end); diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index d295987..3f67b6a 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -1,4 +1,3 @@ -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; diff --git a/lib/widgets/datepicker_widget.dart b/lib/widgets/datepicker_widget.dart index 47c4de6..6ca9fe1 100644 --- a/lib/widgets/datepicker_widget.dart +++ b/lib/widgets/datepicker_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; class DatePickerWidget extends StatelessWidget { diff --git a/lib/widgets/dialogs/confirm_dialog.dart b/lib/widgets/dialogs/confirm_dialog.dart index f753681..ce597c2 100644 --- a/lib/widgets/dialogs/confirm_dialog.dart +++ b/lib/widgets/dialogs/confirm_dialog.dart @@ -15,7 +15,7 @@ class ConfirmDialog extends StatelessWidget { final VoidCallback? onTap; final VoidCallback? onCloseTap; - const ConfirmDialog({Key? key, this.title, required this.message, this.okTitle, this.onTap, this.onCloseTap}) : super(key: key); + const ConfirmDialog({super.key, this.title, required this.message, this.okTitle, this.onTap, this.onCloseTap}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dropdown/dropdown_widget.dart b/lib/widgets/dropdown/dropdown_widget.dart index b4062e4..5ef5e58 100644 --- a/lib/widgets/dropdown/dropdown_widget.dart +++ b/lib/widgets/dropdown/dropdown_widget.dart @@ -19,7 +19,8 @@ class DropdownWidget extends StatelessWidget { final String? selectionCustomIcon; final String? leadingIcon; final Color? labelColor; - + final String? errorMessage; + final bool? hasError; const DropdownWidget( {Key? key, required this.labelText, @@ -34,7 +35,10 @@ class DropdownWidget extends StatelessWidget { this.hasSelectionCustomIcon = false, this.selectionCustomIcon, this.leadingIcon, - this.labelColor}) + this.labelColor, + this.errorMessage, + this.hasError =false + }) : super(key: key); @override @@ -42,16 +46,18 @@ class DropdownWidget extends StatelessWidget { Widget content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: [_buildLabelText(labelColor), _buildDropdown(context)], + children: [_buildLabelText(labelColor), _buildDropdown(context),], ); - return Container( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Container( padding: padding, alignment: Alignment.center, // This might need adjustment based on layout decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: Colors.white, borderRadius: isAllowRadius ? 15.h : null, - side: isBorderAllowed ? BorderSide(color: const Color(0xffefefef), width: 1) : null, + side: isBorderAllowed ? BorderSide(color: hasError! ? Colors.red: const Color(0xffefefef), width: 1) : null, ), child: Row( // Wrap with a Row @@ -62,9 +68,21 @@ class DropdownWidget extends StatelessWidget { SizedBox(width: 3.h), ], Expanded(child: content), + ], ), - ); + ), + if (hasError! && errorMessage != null) + Padding( + padding: EdgeInsets.only(top: 4.h, left: 12.h), // Adjust padding as needed + child: Text( + errorMessage!, + style: TextStyle( + color: Colors.red, + fontSize: 12.f, + ), + ), + )]); } Widget _buildLeadingIcon() { diff --git a/lib/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart b/lib/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart index 73a4a47..b18bd58 100644 --- a/lib/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart +++ b/lib/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart @@ -10,7 +10,7 @@ class ExpandableBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - print("the currently selected item is ${bottomSheetType}"); + print("the currently selected item is $bottomSheetType"); return AnimatedCrossFade( duration: const Duration(milliseconds: 600), firstChild:children[BottomSheetType.FIXED] ?? SizedBox.shrink(), diff --git a/lib/widgets/expandable_list_widget.dart b/lib/widgets/expandable_list_widget.dart new file mode 100644 index 0000000..b948299 --- /dev/null +++ b/lib/widgets/expandable_list_widget.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; + +// ==================== MAIN CUSTOM WIDGET ==================== + +class CustomExpandableList extends StatefulWidget { + final List items; + final ExpansionMode expansionMode; + final EdgeInsetsGeometry? itemPadding; + final EdgeInsetsGeometry? contentPadding; + final Color? dividerColor; + final double dividerHeight; + final bool showDividers; + final Duration animationDuration; + final Curve animationCurve; + final ExpandableListTheme? theme; + final Function(int index, bool isExpanded)? onItemToggled; + + const CustomExpandableList({ + Key? key, + required this.items, + this.expansionMode = ExpansionMode.multiple, + this.itemPadding, + this.contentPadding, + this.dividerColor, + this.dividerHeight = 1.0, + this.showDividers = true, + this.animationDuration = const Duration(milliseconds: 300), + this.animationCurve = Curves.easeInOut, + this.theme, + this.onItemToggled, + }) : super(key: key); + + @override + _CustomExpandableListState createState() => _CustomExpandableListState(); +} + +class _CustomExpandableListState extends State { + late List _expandedStates; + + @override + void initState() { + super.initState(); + _expandedStates = List.generate( + widget.items.length, + (index) => widget.items[index].initiallyExpanded, + ); + } + + void _toggleItem(int index) { + setState(() { + if (widget.expansionMode == ExpansionMode.exactlyOne) { + // Close all others, open this one + for (int i = 0; i < _expandedStates.length; i++) { + _expandedStates[i] = (i == index) ? !_expandedStates[i] : false; + } + } else if (widget.expansionMode == ExpansionMode.atMostOne) { + // Toggle this one, close if opening another + bool wasExpanded = _expandedStates[index]; + for (int i = 0; i < _expandedStates.length; i++) { + if (i == index) { + _expandedStates[i] = !wasExpanded; + } else if (!wasExpanded) { + _expandedStates[i] = false; + } + } + } else { + // Multiple - just toggle the clicked item + _expandedStates[index] = !_expandedStates[index]; + } + }); + + widget.onItemToggled?.call(index, _expandedStates[index]); + } + + @override + Widget build(BuildContext context) { + final theme = widget.theme ?? ExpandableListTheme.defaultTheme(context); + + return Column( + children: List.generate(widget.items.length, (index) { + final item = widget.items[index]; + final isExpanded = _expandedStates[index]; + final isLast = index == widget.items.length - 1; + + return Column( + children: [ + _ExpandableListItemWidget( + title: item.title, + isExpanded: isExpanded, + onTap: () => _toggleItem(index), + padding: widget.itemPadding, + contentPadding: widget.contentPadding, + theme: theme, + leading: item.leading, + trailing: item.trailing, + backgroundColor: item.backgroundColor, + expandedBackgroundColor: item.expandedBackgroundColor, + animationDuration: widget.animationDuration, + animationCurve: widget.animationCurve, + children: item.children, + ), + if (widget.showDividers && !isLast) + Divider( + height: widget.dividerHeight, + color: widget.dividerColor ?? theme.dividerColor, + ).paddingSymmetrical(12.h,0), + ], + ); + }), + ); + } +} + +// ==================== SUPPORTING WIDGETS ==================== + +class _ExpandableListItemWidget extends StatelessWidget { + final Widget title; + final List children; + final bool isExpanded; + final VoidCallback onTap; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? contentPadding; + final ExpandableListTheme theme; + final Widget? leading; + final Widget? trailing; + final Color? backgroundColor; + final Color? expandedBackgroundColor; + final Duration animationDuration; + final Curve animationCurve; + + const _ExpandableListItemWidget({ + super.key, + required this.title, + required this.children, + required this.isExpanded, + required this.onTap, + this.padding, + this.contentPadding, + required this.theme, + this.leading, + this.trailing, + this.backgroundColor, + this.expandedBackgroundColor, + required this.animationDuration, + required this.animationCurve, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: isExpanded + ? (expandedBackgroundColor ?? theme.expandedBackgroundColor) + : (backgroundColor ?? theme.backgroundColor), + child: Column( + children: [ + // Header + InkWell( + onTap: onTap, + child: Padding( + padding: padding ?? theme.itemPadding, + child: Row( + children: [ + if (leading != null) ...[ + leading!, + SizedBox(width: theme.leadingSpacing), + ], + Expanded(child: title), + if (trailing != null) + trailing! + else + AnimatedRotation( + turns: isExpanded ? 0.5 : 0.0, + duration: animationDuration, + curve: animationCurve, + child: theme.defaultTrailingIcon, + ), + ], + ), + ), + ), + + // Content + AnimatedSize( + duration: animationDuration, + curve: animationCurve, + child: Container( + constraints: BoxConstraints( + minHeight: isExpanded ? 0.0 : 0.0, + maxHeight: isExpanded ? double.infinity : 0.0, + ), + child: isExpanded + ? Padding( + padding: contentPadding ?? theme.contentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ) + : SizedBox.shrink(), + ), + ), + ], + ), + ); + } +} + +// ==================== DATA MODELS ==================== + +class ExpandableListItem { + final Widget title; + final List children; + final Widget? leading; + final Widget? trailing; + final bool initiallyExpanded; + final Color? backgroundColor; + final Color? expandedBackgroundColor; + + const ExpandableListItem({ + required this.title, + required this.children, + this.leading, + this.trailing, + this.initiallyExpanded = false, + this.backgroundColor, + this.expandedBackgroundColor, + }); +} + +// ==================== THEME ==================== + +class ExpandableListTheme { + final Color backgroundColor; + final Color expandedBackgroundColor; + final Color dividerColor; + final Widget defaultTrailingIcon; + final EdgeInsetsGeometry itemPadding; + final EdgeInsetsGeometry contentPadding; + final double leadingSpacing; + + const ExpandableListTheme({ + required this.backgroundColor, + required this.expandedBackgroundColor, + required this.dividerColor, + required this.defaultTrailingIcon, + required this.itemPadding, + required this.contentPadding, + this.leadingSpacing = 12.0, + }); + + factory ExpandableListTheme.defaultTheme(BuildContext context) { + final textColor = Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black; + + return ExpandableListTheme( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + dividerColor: Colors.grey.shade300, + defaultTrailingIcon: Icon( + Icons.keyboard_arrow_down, + color: textColor, + size: 24, + ), + itemPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + contentPadding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), + ); + } + + factory ExpandableListTheme.custom({ + Color? backgroundColor, + Color? expandedBackgroundColor, + Color? dividerColor, + Widget? defaultTrailingIcon, + EdgeInsetsGeometry? itemPadding, + EdgeInsetsGeometry? contentPadding, + double? leadingSpacing, + }) { + return ExpandableListTheme( + backgroundColor: backgroundColor ?? Colors.transparent, + expandedBackgroundColor: expandedBackgroundColor ?? Colors.grey.shade50, + dividerColor: dividerColor ?? Colors.grey.shade300, + defaultTrailingIcon: defaultTrailingIcon ?? + Icon(Icons.keyboard_arrow_down, color: Colors.black, size: 24), + itemPadding: itemPadding ?? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + contentPadding: contentPadding ?? const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), + leadingSpacing: leadingSpacing ?? 12.0, + ); + } +} + +// ==================== ENUMS ==================== + +enum ExpansionMode { + multiple, // Multiple items can be expanded + exactlyOne, // Exactly one item expanded at a time + atMostOne, // Zero or one item expanded at a time +} + + + diff --git a/lib/widgets/family_files/family_file_add_widget.dart b/lib/widgets/family_files/family_file_add_widget.dart index 8e57f91..4840ba7 100644 --- a/lib/widgets/family_files/family_file_add_widget.dart +++ b/lib/widgets/family_files/family_file_add_widget.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index 7fc1f2a..1f5d806 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; + /// A customizable line graph widget using `fl_chart`. /// /// Displays a line chart with configurable axis labels, colors, and data points. @@ -56,60 +57,71 @@ class CustomGraph extends StatelessWidget { final FontWeight? bottomLabelFontWeight; final double? leftLabelInterval; final double? leftLabelReservedSize; + final double? bottomLabelReservedSize; + final bool? showGridLines; + final GetDrawingGridLine? getDrawingHorizontalLine; + final double? horizontalInterval; + final double? minY; + final bool showShadow; + final bool showLinePoints; + final double? cutOffY; + final RangeAnnotations? rangeAnnotations; ///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart final Widget Function(double) leftLabelFormatter; - final Widget Function(double , List) bottomLabelFormatter; - + final Widget Function(double, List) bottomLabelFormatter; final Axis scrollDirection; final bool showBottomTitleDates; final bool isFullScreeGraph; final bool makeGraphBasedOnActualValue; - const CustomGraph({ - super.key, - required this.dataPoints, - required this.leftLabelFormatter, - this.width, - required this.scrollDirection, - required this.height, - this.maxY, - this.maxX, - this.showBottomTitleDates = true, - this.isFullScreeGraph = false, - this.spotColor = AppColors.bgGreenColor, - this.graphColor = AppColors.bgGreenColor, - this.graphShadowColor = AppColors.graphGridColor, - this.graphGridColor = AppColors.graphGridColor, - this.bottomLabelColor = AppColors.textColor, - this.bottomLabelFontWeight = FontWeight.w500, - this.bottomLabelSize, - this.leftLabelInterval, - this.leftLabelReservedSize, - this.makeGraphBasedOnActualValue = false, - required this.bottomLabelFormatter, - this.minX, - }); + const CustomGraph( + {super.key, + required this.dataPoints, + required this.leftLabelFormatter, + this.width, + required this.scrollDirection, + required this.height, + this.maxY, + this.maxX, + this.showBottomTitleDates = true, + this.isFullScreeGraph = false, + this.spotColor = AppColors.bgGreenColor, + this.graphColor = AppColors.bgGreenColor, + this.graphShadowColor = AppColors.graphGridColor, + this.graphGridColor = AppColors.graphGridColor, + this.bottomLabelColor = AppColors.textColor, + this.bottomLabelFontWeight = FontWeight.w500, + this.bottomLabelSize, + this.leftLabelInterval, + this.leftLabelReservedSize, + this.bottomLabelReservedSize, + this.makeGraphBasedOnActualValue = false, + required this.bottomLabelFormatter, + this.minX, + this.showGridLines = false, + this.getDrawingHorizontalLine, + this.horizontalInterval, + this.minY, + this.showShadow = false, + this.showLinePoints = false, + this.cutOffY = 0, + this.rangeAnnotations}); @override Widget build(BuildContext context) { - // var maxY = 0.0; - double interval = 20; - return Material( - color: Colors.white, - child: SizedBox( - width: width, - height: height, - child: LineChart( - LineChartData( - minY: 0, - // maxY: ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(), + color: Colors.white, + child: SizedBox( + width: width, + height: height, + child: LineChart( + LineChartData( + minY: minY ?? 0, maxY: maxY, - // minX: dataPoints.first.labelValue - 1, maxX: maxX, - minX: minX , + minX: minX, lineTouchData: LineTouchData( getTouchLineEnd: (_, __) => 0, getTouchedSpotIndicator: (barData, indicators) { @@ -142,11 +154,8 @@ class CustomGraph extends StatelessWidget { final dataPoint = dataPoints[spot.x.toInt()]; return LineTooltipItem( - '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement??""} - ${dataPoint.displayTime}', - TextStyle( - color: Colors.black, - fontSize: 12.f, - fontWeight: FontWeight.w500), + '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', + TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), ); } return null; // hides the rest @@ -158,7 +167,7 @@ class CustomGraph extends StatelessWidget { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: leftLabelReservedSize??80, + reservedSize: leftLabelReservedSize ?? 80, interval: leftLabelInterval ?? .1, // Let fl_chart handle it getTitlesWidget: (value, _) { return leftLabelFormatter(value); @@ -169,9 +178,9 @@ class CustomGraph extends StatelessWidget { axisNameSize: 20, sideTitles: SideTitles( showTitles: showBottomTitleDates, - reservedSize: 20, + reservedSize: bottomLabelReservedSize ?? 20, getTitlesWidget: (value, _) { - return bottomLabelFormatter(value, dataPoints, ); + return bottomLabelFormatter(value, dataPoints); }, interval: 1, // ensures 1:1 mapping with spots ), @@ -188,30 +197,31 @@ class CustomGraph extends StatelessWidget { top: BorderSide.none, ), ), - lineBarsData: _buildColoredLineSegments(dataPoints), + lineBarsData: _buildColoredLineSegments(dataPoints, showLinePoints), gridData: FlGridData( - show: true, + show: showGridLines ?? true, drawVerticalLine: false, - // horizontalInterval: 40, - checkToShowHorizontalLine: (value) => - value >= 0 && value <= 100, - getDrawingHorizontalLine: (value) { - return FlLine( - color: graphGridColor, - strokeWidth: 1, - dashArray: [5, 5], - ); - }, + horizontalInterval: horizontalInterval, + // checkToShowHorizontalLine: (value) => + // value >= 0 && value <= 100, + getDrawingHorizontalLine: getDrawingHorizontalLine ?? + (value) { + return FlLine( + color: graphGridColor, + strokeWidth: 1, + dashArray: [5, 5], + ); + }, ), - ), - ), - )); + rangeAnnotations: rangeAnnotations), + ), + ), + ); } - List _buildColoredLineSegments(List dataPoints) { + List _buildColoredLineSegments(List dataPoints, bool showLinePoints) { final List allSpots = dataPoints.asMap().entries.map((entry) { - double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value; - debugPrint("the value is $value"); + double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; return FlSpot(entry.key.toDouble(), value); }).toList(); @@ -221,17 +231,17 @@ class CustomGraph extends StatelessWidget { isCurved: true, isStrokeCapRound: true, isStrokeJoinRound: true, - barWidth: 4, + barWidth: 2, gradient: LinearGradient( colors: [graphColor, graphColor], begin: Alignment.centerLeft, end: Alignment.centerRight, ), - dotData: FlDotData( - show: false, - ), + dotData: FlDotData(show: showLinePoints), belowBarData: BarAreaData( - show: true, + show: showShadow, + applyCutOffY: cutOffY != null, + cutOffY: cutOffY ?? 0, gradient: LinearGradient( colors: [ graphShadowColor, @@ -246,33 +256,4 @@ class CustomGraph extends StatelessWidget { return data; } - - // Widget buildLabel(String label) { - // return Padding( - // padding: const EdgeInsets.only(right: 8), - // child: Text( - // label, - // style: TextStyle( - // fontSize: leftLabelSize ?? 8.fSize, color: leftLabelColor), - // textAlign: TextAlign.right, - // ), - // ); - // } - - } - -// final List sampleData = [ -// DataPoint( -// value: 20, -// label: 'Jan 2024', -// ), -// DataPoint( -// value: 36, -// label: 'Feb 2024', -// ), -// DataPoint( -// value: 80, -// label: 'This result', -// ), -// ]; diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index 9444159..e25b117 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -147,6 +147,7 @@ void cameraImageAndroid(Function(String, File) image) async { if (base64Encode != null) { image(base64Encode, _image); } + } class _BottomSheet extends StatelessWidget { diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index d943731..94ed9f8 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -43,40 +43,53 @@ class TextInputWidget extends StatelessWidget { final Color? labelColor; final Function(String)? onSubmitted; + // new multiline options + final bool isMultiline; + final int minLines; + final int maxLines; + final Color? hintColor; + final bool? isHideSwitcher; + // final List countryList; // final Function(Country)? onCountryChange; - TextInputWidget( - {super.key, - required this.labelText, - required this.hintText, - this.controller, - this.onChange, - this.onCalendarTypeChanged, - this.prefix, - this.isEnable = true, - this.isBorderAllowed = true, - this.isAllowRadius = true, - this.isReadOnly = false, - this.keyboardType = TextInputType.number, - this.focusNode, - this.autoFocus = false, - this.padding, - this.isAllowLeadingIcon = false, - this.leadingIcon, - this.isCountryDropDown = false, - this.hasError = false, - this.errorMessage, - this.onCountryChange, - this.selectionType, - this.fontSize, - this.isWalletAmountInput = false, - this.suffix, - this.labelColor, - this.onSubmitted - // this.countryList = const [], - // this.onCountryChange, - }); + TextInputWidget({ + super.key, + required this.labelText, + required this.hintText, + this.controller, + this.onChange, + this.onCalendarTypeChanged, + this.prefix, + this.isEnable = true, + this.isBorderAllowed = true, + this.isAllowRadius = true, + this.isReadOnly = false, + this.keyboardType = TextInputType.number, + this.focusNode, + this.autoFocus = false, + this.padding, + this.isAllowLeadingIcon = false, + this.leadingIcon, + this.isCountryDropDown = false, + this.hasError = false, + this.errorMessage, + this.onCountryChange, + this.selectionType, + this.fontSize, + this.isWalletAmountInput = false, + this.suffix, + this.labelColor, + this.onSubmitted, + this.hintColor, + // multiline defaults + this.isMultiline = false, + this.minLines = 3, + this.maxLines = 6, + this.isHideSwitcher, + // this.countryList = const [], + // this.onCountryChange, + }); final FocusNode _focusNode = FocusNode(); @@ -113,7 +126,7 @@ class TextInputWidget extends StatelessWidget { children: [ Container( padding: padding, - height: 64.h, + height: isMultiline ? null : 64.h, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: Colors.white, @@ -203,8 +216,10 @@ class TextInputWidget extends StatelessWidget { switcherIcon: Utils.buildSvgWithAssets(icon: AppAssets.language, width: 24.h, height: 24.h), language: appState.getLanguageCode()!, initialDate: DateTime.now(), + showCalendarToggle: isHideSwitcher == true ? false : true, fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", - okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), + okWidget: + Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), cancelWidget: Padding( padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)), @@ -240,7 +255,7 @@ class TextInputWidget extends StatelessWidget { return TextField( enabled: isEnable, scrollPadding: EdgeInsets.zero, - keyboardType: keyboardType, + keyboardType: isMultiline ? TextInputType.multiline : keyboardType, controller: controller, readOnly: isReadOnly, textAlignVertical: TextAlignVertical.top, @@ -255,11 +270,24 @@ class TextInputWidget extends StatelessWidget { FocusManager.instance.primaryFocus?.unfocus(); }, onSubmitted: onSubmitted, - style: TextStyle(fontSize: fontS, height: isWalletAmountInput! ? 1 / 4 : 0, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -1), + minLines: isMultiline ? minLines : 1, + maxLines: isMultiline ? maxLines : 1, + style: TextStyle( + fontSize: fontS, + height: isMultiline ? 1.2 : (isWalletAmountInput! ? 1 / 4 : 0), + fontWeight: FontWeight.w500, + color: AppColors.textColor, + letterSpacing: -1, + ), decoration: InputDecoration( isDense: true, hintText: hintText, - hintStyle: TextStyle(fontSize: 14.f, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.75), + hintStyle: TextStyle( + fontSize: 14.f, + height: 21 / 16, + fontWeight: FontWeight.w500, + color: hintColor != null ? AppColors.textColor : Color(0xff898A8D), + letterSpacing: -0.75), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, weight: FontWeight.w500), contentPadding: EdgeInsets.zero, diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading_dialog.dart index 3c2440a..dc860b5 100644 --- a/lib/widgets/loading_dialog.dart +++ b/lib/widgets/loading_dialog.dart @@ -3,10 +3,7 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:lottie/lottie.dart'; class LoadingDialog extends StatefulWidget { diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index f655479..96b2fac 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:huawei_map/huawei_map.dart' ; class HMSMap extends StatefulWidget{ @@ -55,7 +55,7 @@ class _HMSMapState extends State { visible: widget.showCenterMarker, child: Align( alignment: Alignment.center, - child: Utils.buildSvgWithAssets(icon: AppAssets.pin_location, width: 24.w, height: 36.h), + child: Icon(Icons.location_pin, size: 36.h, color: AppColors.primaryRedColor).paddingOnly(bottom: 24.h), ), ) ], diff --git a/lib/widgets/map/map.dart b/lib/widgets/map/gms_map.dart similarity index 72% rename from lib/widgets/map/map.dart rename to lib/widgets/map/gms_map.dart index 0c67f1b..f4c3ad5 100644 --- a/lib/widgets/map/map.dart +++ b/lib/widgets/map/gms_map.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.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/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class GMSMap extends StatelessWidget{ Completer? controller; @@ -29,14 +29,15 @@ class GMSMap extends StatelessWidget{ children: [ GoogleMap( mapType: mapType, - zoomControlsEnabled: false, - myLocationEnabled: myLocationEnabled, - myLocationButtonEnabled: false, + zoomControlsEnabled: true, + myLocationEnabled: myLocationEnabled, + myLocationButtonEnabled: false, compassEnabled: compassEnabled, initialCameraPosition: currentLocation, onCameraMove: (value) => onCameraMoved(value), onCameraIdle: ()=>onCameraIdle(), - onMapCreated: (GoogleMapController controller) { + // padding: EdgeInsets.only(bottom: 300.h), + onMapCreated: (GoogleMapController controller) { this.controller?.complete(controller); }, ), @@ -44,10 +45,10 @@ class GMSMap extends StatelessWidget{ visible: showCenterMarker, child: Align( alignment: Alignment.center, - child: Utils.buildSvgWithAssets(icon: AppAssets.pin_location, width: 24.w, height: 36.h), - ), - ) - ], - ); + child: Icon(Icons.location_pin, size: 36.h, color: AppColors.primaryRedColor).paddingOnly(bottom: 24.h), + ), + ) + ], + ); } } \ No newline at end of file diff --git a/lib/widgets/map/location_map_widget.dart b/lib/widgets/map/location_map_widget.dart new file mode 100644 index 0000000..c0eb431 --- /dev/null +++ b/lib/widgets/map/location_map_widget.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:maps_launcher/maps_launcher.dart'; + +/// A reusable location map widget that displays a static map with address and directions button +/// Can be used in any review screen (HHC, CMC, etc.) +class LocationMapWidget extends StatelessWidget { + /// The latitude coordinate of the location + final double latitude; + + /// The longitude coordinate of the location + final double longitude; + + /// The formatted address or location name to display + final String address; + + /// The title to show above the map (e.g., "Service Location", "Hospital Location") + final String? title; + + /// The zoom level for the map (default: 14) + final int zoomLevel; + + /// The size of the map image (default: 350x165) + final String mapSize; + + /// Optional callback when directions button is pressed + /// If not provided, will use default MapsLauncher + final VoidCallback? onDirectionsTap; + + /// Whether to show the address container (default: true) + final bool showAddress; + + /// Custom map type (default: roadmap) + final String mapType; + final EdgeInsets? padding; + + const LocationMapWidget({ + super.key, + required this.latitude, + required this.longitude, + required this.address, + this.title, + this.zoomLevel = 14, + this.mapSize = '350x165', + this.onDirectionsTap, + this.showAddress = true, + this.mapType = 'roadmap', + this.padding, + }); + + void _defaultLaunchDirections() { + if (latitude != 0.0 && longitude != 0.0) { + MapsLauncher.launchCoordinates(latitude, longitude, address); + } + } + + @override + Widget build(BuildContext context) { + final String staticMapUrl = + "https://maps.googleapis.com/maps/api/staticmap?center=$latitude,$longitude&zoom=$zoomLevel&size=$mapSize&maptype=$mapType&markers=color:red%7C$latitude,$longitude&key=${ApiKeyConstants.googleMapsApiKey}"; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: padding ?? EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + if (title != null) ...[ + Text( + title ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w700, + color: AppColors.blackColor, + letterSpacing: -0.5, + ), + ), + SizedBox(height: 12.h), + ], + + // Address display + if (showAddress) ...[ + Row( + children: [ + Expanded( + child: Text( + address, + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.blackColor, + letterSpacing: -0.4, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 16.h), + ], + + // Map display with bottom button overlay + Stack( + children: [ + ClipRRect( + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(24.r), + child: Image.network( + staticMapUrl, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 165.h, + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Center( + child: CircularProgressIndicator( + color: AppColors.primaryRedColor, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 165.h, + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Center( + child: Icon( + Icons.error_outline, + size: 48.h, + color: AppColors.greyTextColor, + ), + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.785, + child: CustomButton( + text: "Get Directions".needTranslation, + onPressed: onDirectionsTap ?? _defaultLaunchDirections, + backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + borderColor: AppColors.textColor.withValues(alpha: 0.01), + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppAssets.directions_icon, + iconColor: AppColors.whiteColor, + iconSize: 14.h, + ).paddingAll(12.h), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/map/map_utility_screen.dart b/lib/widgets/map/map_utility_screen.dart new file mode 100644 index 0000000..19823da --- /dev/null +++ b/lib/widgets/map/map_utility_screen.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/widgets/location_input_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/map/HMSMap.dart'; +import 'package:hmg_patient_app_new/widgets/map/gms_map.dart'; +import 'package:provider/provider.dart'; + +import '../../../widgets/common_bottom_sheet.dart'; + +/// screen to be used to get the location desired by the user +/// to place the values in the request. +/// [confirmButtonString] button text that will be displayed on the button +/// [titleString] bottom sheet title +/// [subTitleString] bottom sheet subtitle for details +/// [onCrossClicked] if something has to be done if the user close the screen +/// [isGmsAvailable] shows if the device that is running the application is GMS or HMS +/// +/// it results [true] if the user clicks on the submit button +/// and [false] if the user closes the screen without giving the consent to proceed for the request +class MapUtilityScreen extends StatelessWidget { + final String confirmButtonString; + final String titleString; + final String subTitleString; + final bool isGmsAvailable; + final VoidCallback? onCrossClicked; + final VoidCallback? onSubmitted; + + const MapUtilityScreen({ + super.key, + required this.confirmButtonString, + required this.titleString, + required this.subTitleString, + required this.isGmsAvailable, + this.onCrossClicked, + this.onSubmitted, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: Padding( + padding: EdgeInsetsDirectional.only(end: 8.h, bottom: 68.h), + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Utils.buildSvgWithAssets(icon: AppAssets.locate_me, width: 24.h, height: 24.h).paddingAll(12.h).onPress(() { + context.read().moveToCurrentLocation(); + }), + ), + ), + bottomSheet: FixedBottomSheet(context), + body: Stack( + children: [ + if (isGmsAvailable) + GMSMap( + currentLocation: context.read().getGMSLocation(), + onCameraMoved: (value) => context.read().handleGMSMapCameraMoved(value), + onCameraIdle: context.read().handleOnCameraIdle, + myLocationEnabled: true, + inputController: context.read().gmsController, + showCenterMarker: true, + ) + else + HMSMap( + currentLocation: context.read().getHMSLocation(), + onCameraMoved: (value) => context.read().handleHMSMapCameraMoved(value), + onCameraIdle: context.read().handleOnCameraIdle, + myLocationEnabled: false, + inputController: context.read().hmsController, + showCenterMarker: true, + ), + Align( + alignment: AlignmentDirectional.topStart, + child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h).onPress(() { + onCrossClicked?.call(); + // context + // .read() + // .flushPickupInformation(); + + Navigator.pop(context, false); + }), + ).paddingOnly(top: 51.h, left: 24.h), + ], + ), + ); + } + + Widget FixedBottomSheet(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: (details) {}, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 24.h, + children: [ + inputFields(context).paddingSymmetrical(16.h, 0.h), + SizedBox( + child: DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.scaffoldBgColor, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.h), + topRight: Radius.circular(24.h), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24.h, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.h, + children: [ + titleString.toText21( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + subTitleString.needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ) + ], + ), + CustomButton( + text: confirmButtonString.needTranslation, + onPressed: () { + if (onSubmitted != null) { + onSubmitted!(); + } else { + Navigator.pop(context, true); + } + }, + ) + ], + ).paddingOnly(top: 24.h, bottom: 32.h, left: 24.h, right: 24.h), + ), + ), + ], + ), + ], + ), + ); + } + + leadingIcon(String leadingIcon) { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.only(right: 10.h), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.h, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: leadingIcon), + ); + } + + textPlaceInput(context) { + return Consumer(builder: (_, vm, __) { + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: TextInputWidget( + labelText: "Enter Pickup Location Manually".needTranslation, + hintText: "Enter Pickup Location".needTranslation, + controller: TextEditingController( + text: vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description, + ), + leadingIcon: AppAssets.location_pickup, + isAllowLeadingIcon: true, + isEnable: false, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ).onPress(() { + openLocationInputBottomSheet(context); + }), + ); + }); + } + + ///decide which field to show first based on the selected calling place + Widget inputFields(BuildContext context) { + return textPlaceInput(context); + } + + openLocationInputBottomSheet(BuildContext context) { + context.read().flushSearchPredictions(); + showCommonBottomSheetWithoutHeight( + title: "".needTranslation, + context, + child: SizedBox( + height: MediaQuery.sizeOf(context).height * .8, + child: LocationInputBottomSheet(), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + } +} diff --git a/lib/widgets/media_viewer/full_screen_image_viewer.dart b/lib/widgets/media_viewer/full_screen_image_viewer.dart new file mode 100644 index 0000000..0fb92f0 --- /dev/null +++ b/lib/widgets/media_viewer/full_screen_image_viewer.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class FullScreenImageViewer extends StatelessWidget { + final bool isSvg; + final String path; + + const FullScreenImageViewer({super.key, required this.isSvg, required this.path}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + leading: IconButton( + icon: Icon(Icons.close, color: AppColors.whiteColor), + onPressed: () => Navigator.of(context).pop(), + ), + ), + backgroundColor: Colors.black, + body: Center( + child: InteractiveViewer( + child: isSvg + ? SvgPicture.asset(path, width: double.infinity, fit: BoxFit.contain) + : Image.asset(width: double.infinity, path, fit: BoxFit.contain), + ), + ), + ); + } +} diff --git a/lib/widgets/order_tracking/request_tracking.dart b/lib/widgets/order_tracking/request_tracking.dart index c0847f9..f07c0b1 100644 --- a/lib/widgets/order_tracking/request_tracking.dart +++ b/lib/widgets/order_tracking/request_tracking.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/order_tracking/order_tracking_state.dart'; +import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; class OrderTrackingWidget extends StatelessWidget { final double? height; @@ -35,9 +36,9 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index) { if (index == 0) { - return oneProgressBar(widthOfOneState, AppColors.primaryRedColor, true); + return StepperWidget(widthOfOneState, AppColors.primaryRedColor, true, height); } else { - return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); + return StepperWidget(widthOfOneState, AppColors.greyLightColor, false, height); } })); } @@ -46,9 +47,9 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ if(index == 0) { - return oneProgressBar(widthOfOneState, AppColors.primaryRedColor, true); + return StepperWidget(widthOfOneState, AppColors.primaryRedColor, true, height); } else { - return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); + return StepperWidget(widthOfOneState, AppColors.greyLightColor, false, height); } }) ); @@ -57,9 +58,9 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ if(index == 1 || index == 0) { - return oneProgressBar(widthOfOneState, AppColors.primaryRedColor, index == 1 ); + return StepperWidget(widthOfOneState, AppColors.primaryRedColor, index == 1, height ); } else { - return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); + return StepperWidget(widthOfOneState, AppColors.greyLightColor, false, height); } }) ); @@ -67,9 +68,9 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ if(index == 2 || index == 1 || index == 0) { - return oneProgressBar(widthOfOneState, AppColors.primaryRedColor, index == 2); + return StepperWidget(widthOfOneState, AppColors.primaryRedColor, index == 2, height); } else { - return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); + return StepperWidget(widthOfOneState, AppColors.greyLightColor, false, height); } }) ); @@ -77,7 +78,7 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ // if(index == 3) { - return oneProgressBar(widthOfOneState, AppColors.successLightColor, index == 3); + return StepperWidget(widthOfOneState, AppColors.successLightColor, index == 3, height); // } else { // return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); // } @@ -87,7 +88,7 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ // if(index == 3) { - return oneProgressBar(widthOfOneState, AppColors.errorColor, index == 3); + return StepperWidget(widthOfOneState, AppColors.errorColor, index == 3, height); // } else { // return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); // } @@ -97,7 +98,7 @@ class OrderTrackingWidget extends StatelessWidget { return Row( children: List.generate(4, (index){ // if(index == 3) { - return oneProgressBar(widthOfOneState, AppColors.errorColor, index == 3); + return StepperWidget(widthOfOneState, AppColors.errorColor, index == 3, height); // } else { // return oneProgressBar(widthOfOneState, AppColors.greyLightColor, false); // } @@ -106,58 +107,6 @@ class OrderTrackingWidget extends StatelessWidget { } } - Widget oneProgressBar(double width, Color color, bool hasThumb) { - return Row( - children: [ - AnimatedSize( - duration: const Duration(seconds:1), - child: SizedBox( - height: 28.h, - width: width, - child: Stack( - clipBehavior: Clip.none, - children: [ - SizedBox( - height: 28.h, - child: Container( - width: width, - height: height, - decoration: BoxDecoration( - color: color, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(30.h) - ), - ), - ), - Visibility( - visible: hasThumb, - child: Positioned( - top: -6.h, // move thumb above bar center - left: width - 22.h, // move to right end - child: thumb(color), - ), - ) - ], - ), - ), - ), - SizedBox(width: 8.h) - ], - ); - } - - Widget thumb(Color color) { - return Container( - width: 18.h, - height: 18.h, - padding: EdgeInsets.zero, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2.h) - ), - ); - } } \ No newline at end of file diff --git a/lib/widgets/radio/custom_radio_button.dart b/lib/widgets/radio/custom_radio_button.dart index 080fcb3..a530102 100644 --- a/lib/widgets/radio/custom_radio_button.dart +++ b/lib/widgets/radio/custom_radio_button.dart @@ -11,14 +11,13 @@ class CustomRadioOption extends StatelessWidget { // final Widget child; // The content of your radio option (e.g., Text, Image) - const CustomRadioOption({ - super.key, - required this.value, - required this.groupValue, - required this.onChanged, - // required this.child, - required this.text, - }); + const CustomRadioOption( + {super.key, + required this.value, + required this.groupValue, + required this.onChanged, + // required this.child, + required this.text}); @override Widget build(BuildContext context) { @@ -31,13 +30,12 @@ class CustomRadioOption extends StatelessWidget { child: Row( children: [ Container( - width: 20.h, - height: 20.h, + width: 18.h, + height: 18.h, decoration: BoxDecoration( - shape: BoxShape.circle, - color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor, - border: Border.all(color: isSelected ? AppColors.primaryRedColor : AppColors.bottomNAVBorder, width: 2.h), - ), + shape: BoxShape.circle, + color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor, + border: Border.all(color: isSelected ? AppColors.primaryRedColor : AppColors.bottomNAVBorder, width: 2.h)), ), SizedBox(width: 8.h), text.toText16(weight: FontWeight.w500), // The provided content diff --git a/lib/widgets/radio_list_tile_widget.dart b/lib/widgets/radio_list_tile_widget.dart new file mode 100644 index 0000000..9025e19 --- /dev/null +++ b/lib/widgets/radio_list_tile_widget.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class RadioListTileWidget extends StatelessWidget { + final T value; + final T? groupValue; + final String title; + final Widget? subtitleWidget; + final String? subtitle; + final ValueChanged onChanged; + final bool enabled; + + const RadioListTileWidget({ + super.key, + required this.value, + required this.groupValue, + required this.title, + this.subtitleWidget, + this.subtitle, + required this.onChanged, + this.enabled = true, + }); + + @override + Widget build(BuildContext context) { + final bool isSelected = value == groupValue; + + return InkWell( + onTap: enabled ? () => onChanged(value) : null, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 6.h), + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Container( + width: 20.h, + height: 20.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : const Color(0xff9E9E9E), + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 10.h, + height: 10.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryRedColor, + ), + ), + ) + : null, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText15(weight: FontWeight.w500, letterSpacing: -0.64), + if (subtitleWidget != null) ...[subtitleWidget!], + if (subtitle != null) ...[ + SizedBox(height: 4.h), + title.toText13( + weight: FontWeight.w400, + color: AppColors.greyTextColor, + letterSpacing: -0.56, + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/shimmer/movies_shimmer_widget.dart b/lib/widgets/shimmer/common_shimmer_widget.dart similarity index 72% rename from lib/widgets/shimmer/movies_shimmer_widget.dart rename to lib/widgets/shimmer/common_shimmer_widget.dart index fb1af25..3d935cb 100644 --- a/lib/widgets/shimmer/movies_shimmer_widget.dart +++ b/lib/widgets/shimmer/common_shimmer_widget.dart @@ -1,19 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:flutter/material.dart'; -class MoviesShimmerWidget extends StatelessWidget { - const MoviesShimmerWidget({super.key}); +class CommonShimmerWidget extends StatelessWidget { + const CommonShimmerWidget({super.key}); @override Widget build(BuildContext context) { return SizedBox( child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), + borderRadius: BorderRadius.all(Radius.circular(10.r)), border: Border.all(color: AppColors.lightGreyEFColor, width: 1), boxShadow: [ BoxShadow( @@ -27,7 +26,11 @@ class MoviesShimmerWidget extends StatelessWidget { padding: const EdgeInsets.all(12.0), child: Column( children: [ - Container(height: 100).toShimmer(), + Container( + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(24.r)), + )).toShimmer(), 16.height, Container(height: 24).toShimmer(), 16.height, diff --git a/lib/widgets/shimmer/vital_sign_shimmer_widget.dart b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart new file mode 100644 index 0000000..2ab4741 --- /dev/null +++ b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class VitalSignShimmerWidget extends StatelessWidget { + const VitalSignShimmerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // BMI Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Height Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Weight Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Blood Pressure Card Shimmer + Expanded(child: _buildShimmerCard()), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Padding( + padding: EdgeInsets.all(12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Icon shimmer + Container( + width: 32.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Label shimmer + Container( + width: 50.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Value shimmer + Container( + width: 40.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Chip shimmer + Container( + width: 45.w, + height: 18.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Arrow shimmer + Align( + alignment: AlignmentDirectional.centerEnd, + child: Container( + width: 10.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ), + ], + ), + ), + ); + } +} + diff --git a/lib/widgets/stepper/stepper_widget.dart b/lib/widgets/stepper/stepper_widget.dart new file mode 100644 index 0000000..5d901ac --- /dev/null +++ b/lib/widgets/stepper/stepper_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class StepperWidget extends StatelessWidget { + + double width = 80.w; + Color activeColor = AppColors.primaryRedColor; + bool hasThumb = true; + double? height = 4.h; + StepperWidget( this.width, this.activeColor, this.hasThumb, this.height, {super.key}); + + @override + Widget build(BuildContext context) { + return oneProgressBar(width, activeColor, hasThumb); + } + + Widget oneProgressBar(double width, Color color, bool hasThumb) { + return Row( + children: [ + AnimatedSize( + duration: const Duration(seconds: 1), + child: SizedBox( + height: 28.h, + width: width, + child: Stack( + clipBehavior: Clip.none, + children: [ + SizedBox( + height: height, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: color, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(30.h) + ), + ), + ), + + Visibility( + visible: hasThumb, + child: Positioned( + top: -6.h, // move thumb above bar center + left: width - 22.h, // move to right end + child: thumb(color), + ), + ) + ], + ), + ), + ), + SizedBox(width: 8.h) + ], + ); + } + + Widget thumb(Color color) { + return Container( + width: 18.h, + height: 18.h, + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2.h) + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3d6604c..461d3ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: # firebase_core: ^3.13.1 permission_handler: ^12.0.1 flutter_local_notifications: ^19.4.1 + timezone: ^0.10.0 provider: ^6.1.5+1 get_it: ^8.2.0 just_audio: ^0.10.4 @@ -45,6 +46,7 @@ dependencies: file_picker: ^10.3.2 local_auth: ^2.3.0 share_plus: ^11.1.0 + device_calendar_plus: ^0.3.1 device_calendar: git: https://github.com/bardram/device_calendar manage_calendar_events: ^2.0.3 @@ -59,7 +61,8 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - flutter_zoom_videosdk: ^2.1.10 + flutter_zoom_videosdk: 2.1.10 + dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 equatable: ^2.0.7 google_api_availability: ^5.0.1 @@ -79,10 +82,12 @@ dependencies: path_provider: ^2.0.8 open_filex: ^4.7.0 flutter_swiper_view: ^1.1.8 + flutter_callkit_incoming: ^3.0.0 location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 + huawei_health: ^6.16.0+300 intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 huawei_map: @@ -90,6 +95,8 @@ dependencies: url: https://github.com/fleoparra/hms-flutter-plugin.git path: flutter-hms-map + scrollable_positioned_list: ^0.3.8 + dev_dependencies: flutter_test: sdk: flutter @@ -108,6 +115,8 @@ flutter: - assets/images/png/body_parts/female/ - assets/images/svg/ - assets/images/png/ + - assets/images/png/zoom/ + - assets/images/png/smartwatches/ - assets/animations/ - assets/animations/lottie/