diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c70123d4..13f778ff 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -36,7 +36,7 @@ android { applicationId = "com.cloudsolutions.HMGPatientApp" // minSdk = 24 minSdk = 29 - targetSdk = 35 + targetSdk = 36 compileSdk = 36 // targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..360ab55d --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,26 @@ + + #ffffff + #ED1C2B + #444647 + #2E3039 + #2E3039 + #a5a5a5 + #e1e1e1 + #FFF + #1e1e1e + #ED1C2B + #F9FAFD + #F2FAFD + #EC2213 + #4D6E6E6E + #FFFFFFFF + #00000000 + #3db39e + #00000000 + #c7c7c7 + #4c4c4c + #4c4c4c + #a5a5a5 + #ED1C2B + #FFF + \ No newline at end of file diff --git a/assets/images/svg/no_image_found.svg b/assets/images/svg/no_image_found.svg new file mode 100644 index 00000000..faea08cd --- /dev/null +++ b/assets/images/svg/no_image_found.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/images/svg/patient_qr_code_icon.svg b/assets/images/svg/patient_qr_code_icon.svg new file mode 100644 index 00000000..c76f0326 --- /dev/null +++ b/assets/images/svg/patient_qr_code_icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 9233f923..971677fa 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -123,7 +123,7 @@ "forgotFileNoTitle": "نسيت الملف الطبي", "enterNationalId": "يرجى إدخال رقم الهاتف المحمول ورقم الهوية", "profileInfo": "يرجى إدخال معلومات الملف الشخصي", - "submit": "بحث", + "submit": "تأكيد", "forgotDesc": "أدخل رقم الهاتف المحمول لاستلام رقم الملف الطبي عبر الرسائل القصيرة", "dob": "تاريخ الميلاد", "hijriDate": "التاريخ الهجري", @@ -1390,7 +1390,6 @@ "medicalFiles": "الملفات الطبية", "addANewFamilyMember": "اضافة", "viewInvoiceDetails": "عرض تفاصيل الفاتورة", - "outPatient": "مريض خارجي", "invoiceDetails": "تفاصيل الفاتورة", "sendingEmailPleaseWait": "جاري إرسال ال��ريد الإلكتروني، يرجى الانتظار...", "emailSentSuccessfullyMessage": "تم إرسال البريد الإلكتروني بنجاح.", @@ -1950,7 +1949,7 @@ "transactions": "المعاملات", "pharmacy": "الصيدلية", "visitsOrders": "الزيارات/الطلبات", - "earned": "حصل", + "earned": "مكتسب", "getParkingQR": "الحصول على رمز الاستجابة السريعة لوقوف السيارات", "cannotOpenThisFile": "لا يمكن فتح هذا الملف", "thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد", @@ -1977,11 +1976,15 @@ "pharmacyRefundPhysicalVisitNotice": "تتطلب استردادات الصيدلية زيارة شخصية لتسليم الأدوية ومعالجة الاسترداد.", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:", - "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", - "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", - "schedule": "جدول", - "doctorSchedule": "جدول الطبيب", - "weeklyShift": "الفترات الأسبوعية", - "dayOff": "إجازة", - "notAvailable": "غير متاح" + "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", + "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", + "schedule": "جدول", + "doctorSchedule": "جدول الطبيب", + "weeklyShift": "الفترات الأسبوعية", + "dayOff": "إجازة", + "notAvailable": "غير متاح", + "hide": "إخفاء", + "viewPatientQR": "عرض رمز الاستجابة السريعة للمريض", + "patientQR": "رمز الاستجابة السريعة للمريض", + "forText": "عن" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 3098d0cb..7bcac22d 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -21,7 +21,7 @@ "mySchedule": "My Schedule", "logout": "Logout", "respirationRate": "Respiration Rate", - "bookAppo": "New Appointment", + "bookAppo": "Book Appointment", "searchBy": "Search By:", "clinic": "Clinic", "byClinic": "By Clinic", @@ -1967,13 +1967,17 @@ "pharmacyRefundPhysicalVisitNotice": "Pharmacy refunds require a physical visit to hand over medications and process the refund.", "parkingQR": "Parking QR Code", "parkingQRDesc": "Scan the below QR Code at the exit parking gate:", - "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.", - "livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?", - "schedule": "Schedule", - "doctorSchedule": "Doctor Schedule", - "weeklyShift": "Weekly Shift", - "dayOff": "Off", - "notAvailable": "Not Available" + "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.", + "livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?", + "schedule": "Schedule", + "doctorSchedule": "Doctor Schedule", + "weeklyShift": "Weekly Shift", + "dayOff": "Off", + "notAvailable": "Not Available", + "hide": "Hide", + "viewPatientQR": "View Patient QR Code", + "patientQR": "Patient QR Code", + "forText": "For" } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fdc3bdfa..f52d12fc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,8 +8,8 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 1587365701FF0D2AAAFED0D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 37D27FAF2C38AC83B852680C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; }; 61243B492EC5FA3700D46FA0 /* PenguinModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B422EC5FA3700D46FA0 /* PenguinModel.swift */; }; @@ -100,7 +100,7 @@ 76B1D2512F91BF8000DBBD1B /* Penguin.xcframework in Frameworks */, 76B1D2552F91BF8000DBBD1B /* PenNavUI.xcframework in Frameworks */, 76B1D2532F91BF8000DBBD1B /* PenguinINRenderer.xcframework in Frameworks */, - DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */, + 37D27FAF2C38AC83B852680C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,10 +339,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -356,10 +360,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -519,7 +527,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 3A359E86ZF; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -529,7 +537,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.12; PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -706,7 +714,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 3A359E86ZF; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -716,7 +724,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.12; PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -736,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 3A359E86ZF; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -746,7 +754,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.12; PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5b3b2681..caa84333 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -86,7 +86,7 @@ NSMicrophoneUsageDescription This app requires microphone access to enable virtual consultation between patient & doctor NSMotionUsageDescription - This app requires motion detection access to function properly. + This app requires access to motion detection to count your daily steps. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. NSPhotoLibraryAddUsageDescription diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index b6480f73..cbfa1b4a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -3,7 +3,7 @@ import 'package:flutter_amazonpaymentservices/environment_type.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { - static const maxSmallScreen = 660; + static const maxSmallScreen = 660; static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; @@ -303,7 +303,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.5; + static final double appVersionID = 21.8; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; @@ -919,8 +919,6 @@ var AUTO_GENERATE_INVOICE_TAMARA = 'Services/PayFort_Serv.svc/REST/Tamara_Getinf var GET_ONESIGNAL_VOIP_TOKEN = 'https://onesignal.com/api/v1/players'; -var CANCEL_PHARMA_LIVECARE_REQUEST = 'https://vcallapi.hmg.com/api/PharmaLiveCare/SendPaymentStatus'; - var INSERT_FREE_SLOTS_LOGS = 'Services/Doctors.svc/Rest/InsertDoctorFreeSlotsLogs'; var GET_NATIONALITY = 'Services/Lists.svc/REST/GetNationality'; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 8db7ec8e..3b242153 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -400,6 +400,8 @@ class AppAssets { static const String habibLogoOnly = '$svgBasePath/habib logo_only.svg'; static const String descriptionIcon = '$svgBasePath/description_icon.svg'; static const String selectImageIcon = '$svgBasePath/select_image_icon.svg'; + static const String patientQRCodeIcon = '$svgBasePath/patient_qr_code_icon.svg'; + static const String noImageFound = '$svgBasePath/no_image_found.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index c4f7402c..e4ca4c25 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -84,6 +84,7 @@ import 'package:hmg_patient_app_new/services/logger_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/services/permission_service.dart'; +import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:local_auth/local_auth.dart'; @@ -155,6 +156,7 @@ class AppDependencies { ); getIt.registerLazySingleton(() => PermissionService()); + getIt.registerLazySingleton(() => TurnstileService(getIt())); // Repositories getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt())); @@ -255,7 +257,8 @@ class AppDependencies { dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), - localAuthService: getIt()), + localAuthService: getIt(), + turnstileService: getIt()), ); getIt.registerLazySingleton(() => ProfileSettingsViewModel( @@ -366,7 +369,11 @@ class AppDependencies { errorHandlerService: getIt(), )); - getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt())); + getIt.registerLazySingleton(() => PayTabsViewModel( + appState: getIt(), + payfortRepo: getIt(), + errorHandlerService: getIt(), + )); getIt.registerLazySingleton( () => ClarityService(appState: getIt.get()), diff --git a/lib/core/services/turnstile_service.dart b/lib/core/services/turnstile_service.dart new file mode 100644 index 00000000..b30113e0 --- /dev/null +++ b/lib/core/services/turnstile_service.dart @@ -0,0 +1,60 @@ +import 'dart:async'; +import 'package:cloudflare_turnstile/cloudflare_turnstile.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Service to handle Cloudflare Turnstile captcha verification +class TurnstileService { + final LoggerService _loggerService; + + // Replace with your Cloudflare Turnstile site key + static const String _siteKey = '0x4AAAAAAAkmbvEy63pcxBBt'; + + TurnstileService(this._loggerService); + + /// Get Turnstile token using invisible mode + /// Returns the token on success, null on failure + Future getTurnstileToken() async { + // Initialize an instance of invisible Cloudflare Turnstile with your site key + final turnstile = CloudflareTurnstile.invisible( + siteKey: _siteKey, + onTimeout: () {}, + options: TurnstileOptions( + retryInterval: const Duration(milliseconds: 2000), + ), + ); + + try { + // Get the Turnstile token + final token = await turnstile.getToken(); + _loggerService.logInfo('Turnstile token retrieved successfully'); + return token; // Return the token upon success + } on TurnstileException catch (e) { + // Handle Turnstile failure + _loggerService.logError('Turnstile challenge failed: ${e.message}'); + if (kDebugMode) { + print('Challenge failed: ${e.message}'); + } + } catch (e) { + // Handle any other exceptions + _loggerService.logError('Unexpected error during Turnstile challenge: $e'); + if (kDebugMode) { + print('Unexpected error: $e'); + } + } finally { + // Ensure the Turnstile instance is properly disposed of + turnstile.dispose(); + } + + // Return null if the token couldn't be generated + return null; + } + + /// Get the site key for Turnstile widget + String getSiteKey() => _siteKey; + + /// Validate if Turnstile is properly configured + bool isConfigured() { + return _siteKey.isNotEmpty && _siteKey != '0x4AAAAAAAkmbvEy63pcxBBt'; + } +} diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index 0994171f..97c8dad0 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -18,7 +18,8 @@ class DateUtil { final endIndex = date.indexOf(end, startIndex + start.length); return DateTime.fromMillisecondsSinceEpoch(int.parse( date.substring(startIndex + start.length, endIndex), - ), isUtc: true).add(Duration(hours: 3)); + // ), isUtc: true).add(Duration(hours: 3)); + ), isUtc: true).toLocal(); } static DateTime convertStringToDateSaudiTimezone(String date, int projectId) { diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index e179a1ab..89f6969a 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -19,7 +19,12 @@ 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/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/utils.dart'; +import 'package:hmg_patient_app_new/features/notifications/models/resp_models/notification_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/notifications/notification_details_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:permission_handler/permission_handler.dart'; import 'package:uuid/uuid.dart'; @@ -523,6 +528,26 @@ class PushNotificationHandler { _incomingCall(remoteMessage.data); // showCallkitIncoming(); } else { + NotificationResponseModel notification = NotificationResponseModel(); + + notification.createdOn = DateUtil.convertDateToString(DateTime.now()); + notification.isSentOn = DateUtil.convertDateToString(DateTime.now()); + notification.messageTypeData = remoteMessage.data['picture']; + notification.messageType = notification.messageTypeData != null && notification.messageTypeData!.isNotEmpty ? "image" : ""; + notification.message = remoteMessage.data['message']; + notification.notificationType = remoteMessage.data["NotificationType"].toString(); + if (remoteMessage.data["NotificationType"] == "2") { + notification.videoURL = remoteMessage.data["VideoUrl"]; + } + + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: NotificationDetailsPage( + notification: notification, + ), + ), + ); + // GetNotificationsResponseModel notification = new GetNotificationsResponseModel(); // // notification.createdOn = DateUtil.convertDateToString(DateTime.now()); diff --git a/lib/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index 4d4ae4b5..62399abd 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -27,7 +27,7 @@ class RequestUtils { }) { bool fileNo = false; if (nationId.isNotEmpty) { - final numericRegex = RegExp(r'^[0-9]+$'); + final numericRegex = RegExp(r'^[0-9]+$'); fileNo = nationId.length < 10 && numericRegex.hasMatch(nationId); //fileNo = nationId.length < 10 && nationId.isNumericOnly() ; if (fileNo) { @@ -40,14 +40,23 @@ class RequestUtils { if (zipCode == "0") { request.patientMobileNumberOthers = phoneNumber; } else { - request.patientMobileNumber = int.parse(phoneNumber); + // Remove any non-numeric characters before parsing + final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + final parsedPhone = int.tryParse(numericPhone); + if (parsedPhone != null) { + request.patientMobileNumber = parsedPhone; + } else { + // If parsing fails, use as string in Others field + request.patientMobileNumberOthers = phoneNumber; + } } } request.oTPSendType = otpTypeEnum.toInt(); // could map OTPTypeEnum if needed request.zipCode = zipCode; // or countryCode if defined elsewhere if (isForRegister) { - request.patientIdentificationID = int.parse(nationId); + final parsedNationId = int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')); + request.patientIdentificationID = parsedNationId ?? 0; request.searchType = 1; request.isHijri = calenderType.toInt; request.patientID = patientId; @@ -56,7 +65,7 @@ class RequestUtils { request.isDentalAllowedBackend = false; } else { if (fileNo) { - request.patientID = patientId ?? int.parse(nationId); + request.patientID = patientId ?? (int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0); request.patientIdentificationID = request.nationalID; request.searchType = 2; } else { @@ -71,15 +80,15 @@ class RequestUtils { static dynamic getCommonRequestWelcome( {required String phoneNumber, - required OTPTypeEnum otpTypeEnum, - required String? deviceToken, - required bool patientOutSA, - required String? loginTokenID, - RegistrationDataModelPayload? registeredData, - int? patientId, - required String nationIdText, - required String countryCode, - required int loginType}) { + required OTPTypeEnum otpTypeEnum, + required String? deviceToken, + required bool patientOutSA, + required String? loginTokenID, + RegistrationDataModelPayload? registeredData, + int? patientId, + required String nationIdText, + required String countryCode, + required int loginType}) { bool fileNo = false; if (nationIdText.isNotEmpty) { final numericRegex = RegExp(r'^[0-9]+$'); @@ -91,8 +100,17 @@ class RequestUtils { request.patientMobileNumberOthers = phoneNumber; request.mobileNo = phoneNumber; } else { - request.patientMobileNumber = int.parse(phoneNumber); - request.mobileNo = '0$phoneNumber'; + // Remove any non-numeric characters before parsing + final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + final parsedPhone = int.tryParse(numericPhone); + if (parsedPhone != null) { + request.patientMobileNumber = parsedPhone; + request.mobileNo = '0$numericPhone'; + } else { + // If parsing fails, use as string in Others field + request.patientMobileNumberOthers = phoneNumber; + request.mobileNo = phoneNumber; + } } request.deviceToken = deviceToken; request.projectOutSA = patientOutSA; @@ -106,8 +124,8 @@ class RequestUtils { request.searchType = registeredData.searchType != null ? registeredData.searchType : fileNo - ? 1 - : 2; + ? 1 + : 2; request.patientID = registeredData.patientId ?? 0; request.patientIdentificationID = request.nationalID = (registeredData.patientIdentificationId ?? 0); request.dob = registeredData.dob; @@ -115,7 +133,8 @@ class RequestUtils { log("nationIdText: ${nationIdText}"); } else { if (fileNo) { - request.patientID = patientId ?? int.parse(nationIdText); + final numericNationId = nationIdText.replaceAll(RegExp(r'[^0-9]'), ''); + request.patientID = patientId ?? (int.tryParse(numericNationId) ?? 0); request.patientIdentificationID = request.nationalID = '0'; request.searchType = 2; //TODO: Issue HEre is Not Login @@ -155,8 +174,17 @@ class RequestUtils { request.patientMobileNumberOthers = mobileNumber; request.mobileNo = mobileNumber; } else { - request.patientMobileNumber = int.parse(mobileNumber); - request.mobileNo = '0$mobileNumber'; + // Remove any non-numeric characters before parsing + final numericMobile = mobileNumber.replaceAll(RegExp(r'[^0-9]'), ''); + final parsedMobile = int.tryParse(numericMobile); + if (parsedMobile != null) { + request.patientMobileNumber = parsedMobile; + request.mobileNo = '0$numericMobile'; + } else { + // If parsing fails, use as string in Others field + request.patientMobileNumberOthers = mobileNumber; + request.mobileNo = mobileNumber; + } } } request.projectOutSA = patientOutSA; @@ -240,16 +268,16 @@ class RequestUtils { "Patientobject": { "TempValue": true, "PatientIdentificationType": (isDubai - ? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1) - : appState.getNHICUserData.idNumber!.substring(0, 1)) == - "1" + ? 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(), + 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') + appState.getUserRegistrationPayload.zipCode == '+966') ? 0 : 1, "FirstNameN": isDubai ? "..." : appState.getNHICUserData.firstNameAr, @@ -266,31 +294,31 @@ class RequestUtils { "DateofBirthN": date, "EmailAddress": emailAddress, "SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || - appState.getUserRegistrationPayload.zipCode == '+966') + appState.getUserRegistrationPayload.zipCode == '+966') ? "1" : "2", "PreferredLanguage": appState.getLanguageCode() == "ar" ? (isDubai ? "1" : 1) : (isDubai ? "2" : 2), "Marital": isDubai ? (maritalStatus == MaritalStatusTypeEnum.single - ? '0' - : maritalStatus == MaritalStatusTypeEnum.married - ? '1' - : '2') + ? '0' + : maritalStatus == MaritalStatusTypeEnum.married + ? '1' + : '2') : (appState.getNHICUserData.maritalStatusCode == 'U' - ? '0' - : appState.getNHICUserData.maritalStatusCode == 'M' - ? '1' - : '2'), + ? '0' + : appState.getNHICUserData.maritalStatusCode == 'M' + ? '1' + : '2'), }, "PatientIdentificationID": - isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), + 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') + appState.getUserRegistrationPayload.zipCode == '+966') ? 0 : 1, "isDentalAllowedBackend": appState.getUserRegistrationPayload.isDentalAllowedBackend, @@ -314,7 +342,9 @@ class RequestUtils { request.sharedPatientId = 0; request.sharedPatientIdentificationId = nationalIDorFile; } else if (loginType == 2) { - request.sharedPatientId = int.parse(nationalIDorFile); + // Remove any non-numeric characters before parsing + final numericId = nationalIDorFile.replaceAll(RegExp(r'[^0-9]'), ''); + request.sharedPatientId = int.tryParse(numericId) ?? 0; request.sharedPatientIdentificationId = ''; } request.searchType = loginType; @@ -325,4 +355,4 @@ class RequestUtils { request.isDentalAllowedBackend = false; return request; } -} +} \ No newline at end of file diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 320bd1cf..b4e91854 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -851,7 +851,7 @@ class Utils { /// Widget to build an image from network - automatically preserves colors in visual modes static Widget buildImgWithNetwork({ - required String url, + required String? url, required Color iconColor, bool isDisabled = false, double? width, @@ -859,6 +859,13 @@ class Utils { BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder, }) { + if (url == null || url.isEmpty) { + return Utils.buildSvgWithAssets( + width: width ?? 24.w, + height: height ?? 24.h, + icon: AppAssets.noImageFound, + ); + } final iconH = height ?? 24.h; final iconW = width ?? 24.w; return PreserveImageColors( @@ -870,7 +877,7 @@ class Utils { errorBuilder: errorBuilder ?? (_, __, ___) { //todo_section change the error builder icon that it is returning - return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon); + return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.noImageFound); }, ), ); diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index c355819e..291e4a9b 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -147,6 +147,9 @@ class AuthenticationRepoImp implements AuthenticationRepo { bool isRegister = false, bool isFormFamilyFile = false, }) async { + + + int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1; sendActivationCodeReq.patientOutSA = isOutKsa; sendActivationCodeReq.isDentalAllowedBackend = false; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b51b36e4..7688cfa4 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:get_it/get_it.dart'; @@ -42,6 +43,7 @@ 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/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; @@ -60,6 +62,7 @@ class AuthenticationViewModel extends ChangeNotifier { final DialogService _dialogService; final NavigationService _navigationService; final LocalAuthService _localAuthService; + final TurnstileService _turnstileService; AuthenticationViewModel({ required AppState appState, @@ -69,12 +72,14 @@ class AuthenticationViewModel extends ChangeNotifier { required NavigationService navigationService, required CacheService cacheService, required LocalAuthService localAuthService, + required TurnstileService turnstileService, }) : _navigationService = navigationService, _dialogService = dialogService, _errorHandlerService = errorHandlerService, _appState = appState, _authenticationRepo = authenticationRepo, - _localAuthService = localAuthService {} + _localAuthService = localAuthService, + _turnstileService = turnstileService {} final TextEditingController nationalIdController = TextEditingController(), phoneNumberController = TextEditingController(), dobController = TextEditingController(), @@ -974,6 +979,17 @@ class AuthenticationViewModel extends ChangeNotifier { // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); + + // if(!kDebugMode) { + // // Get Turnstile token if configured + // final turnstileToken = await _turnstileService.getTurnstileToken(); + // if (turnstileToken != null) { + // request.turnstileToken = turnstileToken; + // } else { + // request.turnstileToken = ""; + // } + // } + if (isNeedLoading) { LoaderBottomSheet.showLoader(); } @@ -985,7 +1001,10 @@ class AuthenticationViewModel extends ChangeNotifier { sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isFormFamilyFile: isFormFamilyFile); resultEither.fold( - (failure) async => await _errorHandlerService.handleError(failure: failure), + (failure) async { + LoaderBottomSheet.hideLoader(); + await _errorHandlerService.handleError(failure: failure); + }, (apiResponse) async { if (isFormFamilyFile) { if (apiResponse.data['PatientShareRequestID'] != null && apiResponse.data['PatientShareRequestID'] == 0 && apiResponse.messageStatus == 1) { diff --git a/lib/features/authentication/models/request_models/send_activation_request_model.dart b/lib/features/authentication/models/request_models/send_activation_request_model.dart index 2908f7ec..6e2ddb93 100644 --- a/lib/features/authentication/models/request_models/send_activation_request_model.dart +++ b/lib/features/authentication/models/request_models/send_activation_request_model.dart @@ -30,6 +30,7 @@ class SendActivationRequest { int? status; int? familyRegionID; bool? isPatientExcluded; + String? turnstileToken; SendActivationRequest( {this.patientMobileNumber, @@ -62,7 +63,8 @@ class SendActivationRequest { this.responseID, this.status, this.familyRegionID, - this.isPatientExcluded}); + this.isPatientExcluded, + this.turnstileToken}); SendActivationRequest.fromJson(Map json) { patientMobileNumber = json['PatientMobileNumber']; @@ -96,6 +98,7 @@ class SendActivationRequest { status = json['Status']; familyRegionID = json['FamilyRegionID']; isPatientExcluded = json['IsPatientExcluded']; + turnstileToken = json['TurnstileToken']; } Map toJson() { @@ -135,6 +138,9 @@ class SendActivationRequest { data['Status'] = status; data['FamilyRegionID'] = familyRegionID; data['IsPatientExcluded'] = isPatientExcluded; + if (turnstileToken != null) { + data['TurnstileToken'] = turnstileToken; + } return data; } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index e4f028d9..4831901d 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -404,6 +404,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { isWaitingAppointmentSelected = false; isPatientRescheduleAppointment = false; bodyTypes = [maleLaserCategory, femaleLaserCategory]; + selectedChiefComplaintID = 0; // getLocation(); notifyListeners(); } @@ -420,14 +421,14 @@ class BookAppointmentsViewModel extends ChangeNotifier { setIsNearestAppointmentSelected(bool isNearestAppointmentSelected) { this.isNearestAppointmentSelected = isNearestAppointmentSelected; if (isNearestAppointmentSelected) { - for (var group in doctorsListGrouped) { - group.sort((a, b) { - var aSlot = a.nearestFreeSlot; - var bSlot = b.nearestFreeSlot; - if (aSlot == null || bSlot == null) return 0; - return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot)); - }); - } + // for (var group in doctorsListGrouped) { + // group.sort((a, b) { + // var aSlot = a.nearestFreeSlot; + // var bSlot = b.nearestFreeSlot; + // if (aSlot == null || bSlot == null) return 0; + // return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot)); + // }); + // } } else { // for (var group in doctorsListGrouped) { // group.sort((a, b) { @@ -675,7 +676,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { (failure) async { isDoctorsListLoading = false; if (onError != null) onError(LocaleKeys.noDoctorFound.tr()); - notifyListeners(); }, (apiResponse) { @@ -690,7 +690,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { clearSearchFilters(); getFiltersFromDoctorList(); _groupDoctorsList(); - setIsNearestAppointmentSelected(isNearest); + // setIsNearestAppointmentSelected(isNearest); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart index 1b460a03..15f937e9 100644 --- a/lib/features/contact_us/contact_us_view_model.dart +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -12,8 +12,22 @@ import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_p import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_status_coc_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/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:permission_handler/permission_handler.dart'; +/// Class to store feedback attachment data +class FeedbackAttachment { + final String base64String; + final File file; + final String fileName; + + FeedbackAttachment({ + required this.base64String, + required this.file, + required this.fileName, + }); +} + class ContactUsViewModel extends ChangeNotifier { ContactUsRepo contactUsRepo; ErrorHandlerService errorHandlerService; @@ -34,7 +48,7 @@ class ContactUsViewModel extends ChangeNotifier { String? chatRequestID; - List feedbackAttachmentList = []; + List feedbackAttachmentList = []; List cocItemsList = []; bool isCOCItemsListLoading = false; @@ -167,13 +181,20 @@ class ContactUsViewModel extends ChangeNotifier { notifyListeners(); } - addFeedbackAttachment(String attachmentPath) { - feedbackAttachmentList.add(attachmentPath); + addFeedbackAttachment(String base64String, File file) { + // Extract filename from file path + final fileName = file.path.split('/').last; + + feedbackAttachmentList.add(FeedbackAttachment( + base64String: base64String, + file: file, + fileName: fileName, + )); notifyListeners(); } - removeFeedbackAttachment(String attachmentPath) { - feedbackAttachmentList.remove(attachmentPath); + removeFeedbackAttachment(FeedbackAttachment attachment) { + feedbackAttachmentList.remove(attachment); notifyListeners(); } @@ -281,7 +302,7 @@ class ContactUsViewModel extends ChangeNotifier { 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.attachment = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first.base64String : ""; requestInsertCOCItem.title = subject; requestInsertCOCItem.details = message; requestInsertCOCItem.cOCTypeName = selectedFeedbackType.id.toString(); @@ -290,7 +311,7 @@ class ContactUsViewModel extends ChangeNotifier { requestInsertCOCItem.isUserLoggedIn = true; requestInsertCOCItem.projectID = 0; requestInsertCOCItem.patientName = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}"; - requestInsertCOCItem.fileName = "Image 1"; + requestInsertCOCItem.fileName = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first.fileName : ""; requestInsertCOCItem.appVersion = ApiConsts.appVersionID; requestInsertCOCItem.uILanguage = appState.isArabic() ? "ar" : "en"; requestInsertCOCItem.browserInfo = Platform.localHostname; @@ -308,7 +329,15 @@ class ContactUsViewModel extends ChangeNotifier { final result = await contactUsRepo.insertCOCItem(requestInsertCOCItem: requestInsertCOCItem, patientSelectedAppointment: patientFeedbackSelectedAppointment); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + LoaderBottomSheet.hideLoader(); + await errorHandlerService.handleError(failure: failure); + }, + // (failure) async { + // if(onError != null) { + // onError(failure.toString()); + // } + // }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 621ebe4c..2422920a 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -33,14 +33,10 @@ class InsuranceViewModel extends ChangeNotifier { InsuranceViewModel({required this.insuranceRepo, required this.errorHandlerService}); initInsuranceProvider() { - debugPrint("InsuranceViewModel: initInsuranceProvider called, isInsuranceDataToBeLoaded=$isInsuranceDataToBeLoaded"); if (isInsuranceDataToBeLoaded) { patientInsuranceList.clear(); isInsuranceLoading = true; - debugPrint("InsuranceViewModel: Calling getPatientInsuranceDetails API"); getPatientInsuranceDetails(); - } else { - debugPrint("InsuranceViewModel: Skipping API call, isInsuranceDataToBeLoaded is false"); } patientInsuranceCardHistoryList.clear(); isInsuranceHistoryLoading = true; @@ -82,17 +78,14 @@ class InsuranceViewModel extends ChangeNotifier { Future getPatientInsuranceDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { if (!isInsuranceDataToBeLoaded) { - debugPrint("InsuranceViewModel: getPatientInsuranceDetails - skipping, flag is false"); return; } - debugPrint("InsuranceViewModel: getPatientInsuranceDetails - making API call"); final result = await insuranceRepo.getPatientInsuranceDetails(); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { - debugPrint("InsuranceViewModel: API call failed - ${failure.toString()}"); isInsuranceLoading = false; isInsuranceDataToBeLoaded = true; isInsuranceExpired = false; @@ -102,11 +95,9 @@ class InsuranceViewModel extends ChangeNotifier { }, (apiResponse) { if (apiResponse.messageStatus == 2) { - debugPrint("InsuranceViewModel: API returned error status"); isInsuranceDataToBeLoaded = false; // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { - debugPrint("InsuranceViewModel: API success - received ${apiResponse.data?.length ?? 0} insurance records"); patientInsuranceList = apiResponse.data!; isInsuranceLoading = false; isInsuranceDataToBeLoaded = false; @@ -115,7 +106,6 @@ class InsuranceViewModel extends ChangeNotifier { isInsuranceExpired = DateTime.now().isAfter( DateUtil.convertStringToDate(patientInsuranceList.first.cardValidTo), ); - debugPrint("InsuranceViewModel: Insurance card expired: $isInsuranceExpired"); } isInsuranceExpiryBannerShown = isInsuranceExpired; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 82468ada..94679047 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -29,6 +29,7 @@ abstract class LabRepo { Future>> getPatientAiOverViewLabOrders({required int langId, required dynamic payload}); Future>> getPatientAiOverViewLabOrder(var payloa); + Map> groupLabResultsByPackageName(List labResults); } class LabRepoImp implements LabRepo { @@ -37,6 +38,55 @@ class LabRepoImp implements LabRepo { LabRepoImp({required this.loggerService, required this.apiClient}); + /// Groups a list of LabResult objects by their PackageName + /// Returns a Map where the key is the PackageName and the value is a List of LabResult objects + /// Both the keys and the lists are sorted alphabetically + /// + /// Example usage: + /// ```dart + /// List labResults = [/* your lab results */]; + /// Map> groupedResults = labRepo.groupLabResultsByPackageName(labResults); + /// + /// // Access results by package name + /// groupedResults.forEach((packageName, results) { + /// print('Package: $packageName has ${results.length} results'); + /// }); + /// ``` + Map> groupLabResultsByPackageName(List labResults) { + Map> groupedResults = {}; + + // Group results by package name + for (var labResult in labResults) { + String packageName = labResult.PackageName ?? 'Unknown Package'; + + if (groupedResults.containsKey(packageName)) { + // Package name exists, add to existing list + groupedResults[packageName]!.add(labResult); + } else { + // Package name doesn't exist, create new key with new list + groupedResults[packageName] = [labResult]; + } + } + + // Sort each list alphabetically by description + groupedResults.forEach((key, list) { + list.sort((a, b) { + String descA = (a.description ?? '').toLowerCase(); + String descB = (b.description ?? '').toLowerCase(); + return descA.compareTo(descB); + }); + }); + + // Sort the map keys alphabetically and return a new sorted map + var sortedKeys = groupedResults.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + Map> sortedGroupedResults = {}; + for (var key in sortedKeys) { + sortedGroupedResults[key] = groupedResults[key]!; + } + + return sortedGroupedResults; + } + @override Future>>> getPatientLabOrders() async { Map mapDevice = {}; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 09caf578..1c25ef9d 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -57,7 +57,11 @@ class LabViewModel extends ChangeNotifier { PatientLabOrdersResponseModel? currentlySelectedPatientOrder; - List mainLabResultsByHospitals = []; + // Flat list for UI compatibility + List mainLabResultsByHospitalsList = []; + + // Grouped map by package name for efficient lookups + Map> mainLabResultsByPackageName = {}; List mainLabResults = []; List mainGraphPoints = []; @@ -383,7 +387,8 @@ class LabViewModel extends ChangeNotifier { Future getPatientLabResultByHospital(PatientLabOrdersResponseModel laborder) async { isLabResultByHospitalLoading = true; notifyListeners(); - mainLabResultsByHospitals.clear(); + mainLabResultsByHospitalsList.clear(); + mainLabResultsByPackageName.clear(); final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); @@ -397,7 +402,9 @@ class LabViewModel extends ChangeNotifier { isLabResultByHospitalLoading = false; if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - mainLabResultsByHospitals = apiResponse.data ?? []; + mainLabResultsByHospitalsList = apiResponse.data ?? []; + // Group the results by package name for efficient lookups + mainLabResultsByPackageName = labRepo.groupLabResultsByPackageName(mainLabResultsByHospitalsList); notifyListeners(); } }, @@ -842,6 +849,25 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + // /// Finds a matching LabResult from the grouped map by package name + // /// Searches through all packages to find a result matching the test code or description + // LabResult? _findMatchingLabResult(TestDetails testDetail) { + // // Search through all packages in the grouped map + // for (var packageResults in mainLabResultsByPackageName.values) { + // try { + // var matchingResult = packageResults.firstWhere( + // (labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, + // ); + // return matchingResult; + // } catch (e) { + // // Continue searching in other packages + // continue; + // } + // } + // // Return null if no match found + // return LabResult(); + // } + Future getAiOverviewLabOrders({required int langId, required PatientLabOrdersResponseModel labOrder, required String loadingText}) async { isLabAIAnalysisNeedsToBeShown = true; notifyListeners(); @@ -858,7 +884,7 @@ class LabViewModel extends ChangeNotifier { if (labOrder.testDetails != null && labOrder.testDetails!.isNotEmpty) { for (var testDetail in labOrder.testDetails!) { LabResult? matchingResult = - mainLabResultsByHospitals.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); + mainLabResultsByHospitalsList.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); testResults.add({ "test_name": testDetail.description ?? "", "result_value": matchingResult.resultValue ?? "", diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index 2808dce9..78be004a 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -28,6 +28,7 @@ class LabResult { String? packageShortDescription; int? testId; String? testShortDescription; + String? PackageName; LabResult( {this.description, @@ -56,6 +57,7 @@ class LabResult { this.referenceLow, this.testId, this.testShortDescription, + this.PackageName }); LabResult.fromJson(Map json) { @@ -88,6 +90,7 @@ class LabResult { resultTypeID = json['ResultTypeID']; testId = json['TestID']; testShortDescription = json['TestShortDescription']; + PackageName = json['PackageName']; } Map toJson() { @@ -118,6 +121,7 @@ class LabResult { data['ReferenceLow'] = this.referenceLow; data['TestID'] = this.testId; data['TestShortDescription'] = this.testShortDescription; + data['PackageName'] = this.PackageName; return data; } diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 41a726d9..6fb8c8a2 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -29,7 +29,11 @@ class MedicalFileViewModel extends ChangeNotifier { bool isPatientSickLeavePDFLoading = false; bool isPatientMedicalReportsListLoading = false; bool isPatientAllergiesListLoading = false; + bool _isFamilyFilesLoading = false; + bool get isFamilyFilesLoading => _isFamilyFilesLoading; + + set setIsFamilyFilesLoading(bool value) => _isFamilyFilesLoading = value; MedicalFileRepo medicalFileRepo; ErrorHandlerService errorHandlerService; @@ -42,7 +46,7 @@ class MedicalFileViewModel extends ChangeNotifier { bool isSickLeavesSortByClinic = true; - // bool isSickLeavesDataNeedsReloading = true; + bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -165,15 +169,15 @@ class MedicalFileViewModel extends ChangeNotifier { } setIsPatientSickLeaveListLoading(bool val) { - // if (val && isSickLeavesDataNeedsReloading) { - patientSickLeaveList.clear(); - patientSickLeavesByClinic.clear(); + if (val && isSickLeavesDataNeedsReloading) { + patientSickLeaveList.clear(); + patientSickLeavesByClinic.clear(); patientSickLeavesByHospital.clear(); patientSickLeavesViewList.clear(); patientSickLeavePDFBase64 = ""; isSickLeavesSortByClinic = true; isPatientSickLeaveListLoading = val; - // } + } notifyListeners(); } @@ -282,9 +286,9 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isSickLeavesDataNeedsReloading) { - // return; - // } + if (!isSickLeavesDataNeedsReloading) { + return; + } patientSickLeaveList.clear(); notifyListeners(); @@ -300,6 +304,7 @@ class MedicalFileViewModel extends ChangeNotifier { // ), (failure) async { isPatientSickLeaveListLoading = false; + isSickLeavesDataNeedsReloading = true; notifyListeners(); }, (apiResponse) { @@ -330,7 +335,7 @@ class MedicalFileViewModel extends ChangeNotifier { } } patientSickLeavesViewList = patientSickLeavesByClinic; - // isSickLeavesDataNeedsReloading = false; + isSickLeavesDataNeedsReloading = false; notifyListeners(); if (onSuccess != null) { @@ -430,21 +435,42 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getFamilyFiles({int? status, Function(dynamic)? onSuccess, Function(String)? onError}) async { + + setIsFamilyFilesLoading = true; + + final shouldShowLoader = patientFamilyFiles.isEmpty; + if (shouldShowLoader) { + LoaderBottomSheet.showLoader(); + } + final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.getSuperUserID != null ? _appState.getSuperUserID! : _appState.getAuthenticatedUser()!.patientId!); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), + (failure) async { + + setIsFamilyFilesLoading = false; + + await errorHandlerService.handleError( + failure: failure, + onOkPressed: () { + if (shouldShowLoader) { + LoaderBottomSheet.hideLoader(); + } + onError!(failure.message); + }, + ); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { + + setIsFamilyFilesLoading = false; + // _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); + if (shouldShowLoader) { + LoaderBottomSheet.hideLoader(); + } } else if (apiResponse.messageStatus == 1) { if (apiResponse.data != null) { - // Add current user as the first active family file final currentUser = _appState.getAuthenticatedUser()!; final currentUserFamilyFile = FamilyFileResponseModelLists( patientId: currentUser.patientId, @@ -524,6 +550,12 @@ class MedicalFileViewModel extends ChangeNotifier { pendingFamilyFiles = newPendingFamilyFiles; } + // Clear loading state on success + setIsFamilyFilesLoading = false; + + if (shouldShowLoader) { + LoaderBottomSheet.hideLoader(); + } notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -661,13 +693,10 @@ class MedicalFileViewModel extends ChangeNotifier { } Future handleFamilyFileRequestOTPVerification() async { - LoaderBottomSheet.showLoader(); if (!_appState.getIsChildLoggedIn) { await getFamilyFiles(status: 0); await getAllPendingRecordsByResponseId(); } - - LoaderBottomSheet.hideLoader(); } Future removeFileFromFamilyMembers({int? id}) async { diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index d7a025fc..87fb2ec2 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -25,7 +25,7 @@ import 'models/resp_models/appointment_details_resp_model.dart'; abstract class MyAppointmentsRepo { Future>>> getPatientAppointments( - {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}); Future>> getPatientShareAppointment( {required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment}); @@ -53,7 +53,7 @@ abstract class MyAppointmentsRepo { Future>>> getPatientAppointmentsForTimeLine(); - Future>>> getPatientDoctorsList(); + Future>>> getPatientDoctorsList({bool isTop8 = false}); Future>>> getFavouriteDoctorsList(); @@ -105,14 +105,15 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { @override Future>>> getPatientAppointments( - {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, "IsComingFromCOC": false, "isForUpcomming": false, "IsForMedicalReport": false, "IsForArrived": isArrivedAppointments, - "IsIrisPrescription": isForEyeMeasurement + "IsIrisPrescription": isForEyeMeasurement, + "IsForTimeLine": isForTimeLine }; try { @@ -172,9 +173,9 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { try { final list = response['OnlineCheckInAppointments']; patientShareObj = (list == null || list.isEmpty) ? PatientAppointmentShareResponseModel() : PatientAppointmentShareResponseModel.fromJson(list[0]); - patientShareObj.isCash = response["IsCash"]; - patientShareObj.isEligible = response["IsEligible"]; - patientShareObj.isInsured = response["IsInsured"]; + patientShareObj.isCash = isLiveCareAppointment ? list[0]["IsCash"] : response["IsCash"]; + patientShareObj.isEligible = isLiveCareAppointment ? list[0]["IsEligible"] : response["IsEligible"]; + patientShareObj.isInsured = isLiveCareAppointment ? list[0]["IsInsured"] : response["IsInsured"]; // if (list == null || list.isEmpty) { // throw Exception("patient share list is empty"); @@ -514,11 +515,12 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { } @override - Future>>> getPatientDoctorsList() async { + Future>>> getPatientDoctorsList({bool isTop8 = false}) async { Map mapDevice = { "Top": 0, "beforeDays": 0, "exludType": 4, + "IsTop8": isTop8, }; try { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 9182f9e7..832d94fe 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -54,6 +54,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isFavouriteDoctorsDataFetched = false; bool isAppointmentDataToBeLoaded = true; + bool isMyDoctorsDataToBeLoaded = true; + bool isArrivedAppointmentDataLoaded = false; + bool isFullArrivedAppointmentsLoaded = false; bool isEyeMeasurementsAppointmentsLoading = false; @@ -75,7 +78,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientUpcomingAppointmentsHistoryList = []; List patientArrivedAppointmentsHistoryList = []; - List patientTimelineAppointmentsList = []; + // List patientAllArrivedAppointmentsHistoryList = []; List patientMyDoctorsList = []; @@ -171,11 +174,12 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); - patientTimelineAppointmentsList.clear(); + // patientAllArrivedAppointmentsHistoryList.clear(); patientEyeMeasurementsAppointmentsHistoryList.clear(); isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; patientMyDoctorsList.clear(); + isFullArrivedAppointmentsLoaded = false; } isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; @@ -210,9 +214,15 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsMyDoctorsDataToBeLoaded(bool val) { + isMyDoctorsDataToBeLoaded = val; + notifyListeners(); + } + setIsAppointmentDataToBeLoaded(bool val) { if (val) { isMyAppointmentsLoading = true; + isMyDoctorsDataToBeLoaded = true; } isAppointmentDataToBeLoaded = val; notifyListeners(); @@ -259,7 +269,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {bool isForTimeLine = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { if (patientArrivedAppointmentsHistoryList.isNotEmpty) { isPatientHasQueueAppointment = false; notifyListeners(); @@ -268,15 +278,19 @@ class MyAppointmentsViewModel extends ChangeNotifier { getPatientAppointmentQueueDetails(); } } - if (!isAppointmentDataToBeLoaded) return; - debugPrint('getPatientAppointments CALLED!!!'); + // Skip API call if full data is already loaded and we're not requesting timeline view + if (!isForTimeLine && isFullArrivedAppointmentsLoaded && !isAppointmentDataToBeLoaded) { + return; + } + + if (!isAppointmentDataToBeLoaded) return; patientAppointmentsByClinic.clear(); patientAppointmentsByHospital.clear(); patientAppointmentsViewList.clear(); - patientTimelineAppointmentsList.clear(); + // patientAllArrivedAppointmentsHistoryList.clear(); filteredAppointmentList.clear(); patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); @@ -285,7 +299,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { final results = await Future.wait([ myAppointmentsRepo.getPatientAppointments(isActiveAppointment: isActiveAppointment, isArrivedAppointments: isArrivedAppointments), - myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true), + myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForTimeLine: isForTimeLine), ]); results[0].fold( @@ -314,6 +328,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientArrivedAppointmentsHistoryList = apiResponse.data!; isMyAppointmentsLoading = false; isAppointmentDataToBeLoaded = false; + if (!isForTimeLine) { + // patientAllArrivedAppointmentsHistoryList = apiResponse.data!; + isArrivedAppointmentDataLoaded = true; + isFullArrivedAppointmentsLoaded = true; + } notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -340,13 +359,73 @@ class MyAppointmentsViewModel extends ChangeNotifier { } // } - debugPrint('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); - debugPrint('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); - debugPrint('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); notifyListeners(); } + changeTabToArrived() { + if (isFullArrivedAppointmentsLoaded && patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { + selectedTabIndex = 1; + updateListWRTTab(1); + notifyListeners(); + } + } + + /// Loads full arrived appointments list if not already loaded + /// This is called when navigating to My Appointments page from landing page + Future loadFullArrivedAppointmentsIfNeeded({Function(dynamic)? onSuccess, Function(String)? onError}) async { + // Skip if already loaded + if (isFullArrivedAppointmentsLoaded) { + // Auto-switch to Arrived tab if no upcoming appointments (even when data is already loaded) + if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { + selectedTabIndex = 1; + updateListWRTTab(1); + notifyListeners(); + } + return; + } + + setIsAppointmentsHistoryLoading(true); + + // Fetch full arrived appointments (isForTimeLine = false) + final result = await myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForTimeLine: false); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // Handle error + } else if (apiResponse.messageStatus == 1) { + patientArrivedAppointmentsHistoryList = apiResponse.data!; + // patientAllArrivedAppointmentsHistoryList = apiResponse.data!; + isArrivedAppointmentDataLoaded = true; + isFullArrivedAppointmentsLoaded = true; + + // Rebuild the combined list + patientAppointmentsHistoryList.clear(); + patientAppointmentsHistoryList.addAll(patientUpcomingAppointmentsHistoryList); + patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); + + isMyAppointmentsLoading = false; + + // Auto-switch to Arrived tab if no upcoming appointments + if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { + selectedTabIndex = 1; + updateListWRTTab(1); + } else { + // Update filtered list based on current tab + updateListWRTTab(selectedTabIndex); + } + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + void getFiltersForSelectedAppointmentList(List filteredAppointmentList) { availableFilters.clear(); if (filteredAppointmentList.isEmpty == true) return; @@ -355,9 +434,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { availableFilters.add(AppointmentListingFilters.LIVECARE); } - if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) { - availableFilters.add(AppointmentListingFilters.WALKIN); - } + // if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) { + // availableFilters.add(AppointmentListingFilters.WALKIN); + // } if (filteredAppointmentList.any((element) => AppointmentType.isArrived(element) == true)) { availableFilters.add(AppointmentListingFilters.ARRIVED); @@ -673,13 +752,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isAppointmentDataToBeLoaded) return; + Future getPatientMyDoctors({bool isTop8 = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { + if (!isMyDoctorsDataToBeLoaded) return; isPatientMyDoctorsLoading = true; patientMyDoctorsList.clear(); notifyListeners(); - final result = await myAppointmentsRepo.getPatientDoctorsList(); + final result = await myAppointmentsRepo.getPatientDoctorsList(isTop8: isTop8); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), @@ -692,6 +771,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; + isMyDoctorsDataToBeLoaded = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -793,15 +873,15 @@ class MyAppointmentsViewModel extends ChangeNotifier { selectedFilter = []; // if(previouslySelectedTab == selectedTabIndex ) return; switch (index) { + // case 0: + // filteredAppointmentList.clear(); + // filteredAppointmentList.addAll(patientAppointmentsHistoryList); + // break; case 0: - filteredAppointmentList.clear(); - filteredAppointmentList.addAll(patientAppointmentsHistoryList); - break; - case 1: filteredAppointmentList.clear(); filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList); break; - case 2: + case 1: filteredAppointmentList.clear(); filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList); break; @@ -827,11 +907,12 @@ class MyAppointmentsViewModel extends ChangeNotifier { this.end = end; isDateFilterSelected = true; List sourceList = []; - if (selectedTabIndex == 0) { - sourceList = patientAppointmentsHistoryList; - } else if (selectedTabIndex == 1) { + // if (selectedTabIndex == 0) { + // sourceList = patientAppointmentsHistoryList; + // } else + if (selectedTabIndex == 0) { sourceList = patientUpcomingAppointmentsHistoryList; - } else if (selectedTabIndex == 2) { + } else if (selectedTabIndex == 1) { sourceList = patientArrivedAppointmentsHistoryList; } // if (isDateFilterSelected) sourceList = filteredAppointmentList; @@ -1291,7 +1372,6 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } catch (e) { // Ignore errors if reminder doesn't exist - debugPrint("No existing ${type.displayName} to remove"); } } } diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index 35a924af..508753ef 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -11,17 +11,29 @@ import 'package:flutter_paytabs_bridge/PaymentSdkTransactionType.dart'; import 'package:flutter_paytabs_bridge/flutter_paytabs_bridge.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/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/paytabs_config.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; class PayTabsViewModel extends ChangeNotifier { late PaymentSdkConfigurationDetails paymentConfiguration; final AppState appState; + final PayfortRepo payfortRepo; + final ErrorHandlerService errorHandlerService; late PaytabsTransactionResponseModel paytabsTransactionResponseModel; PayTabsViewModel({ required this.appState, + required this.payfortRepo, + required this.errorHandlerService, }); setPaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) { @@ -152,6 +164,42 @@ class PayTabsViewModel extends ChangeNotifier { notifyListeners(); } + Future paytabsRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel); + + 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 paytabsResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); + + 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); + } + } + }, + ); + } + startApplePayPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) async { bool _callbackFired = false; // prevent duplicate callback invocations @@ -189,41 +237,132 @@ class PayTabsViewModel extends ChangeNotifier { }); } - startCardPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) { + startCardPayment({ + required String paymentOption, + required String liveServiceID, + required String isScheduleLiveCare, + required String appointmentDate, + required String appointmentNo, + required String doctorID, + required String transactionID, + required String amount, + required String fileNumber, + required String orderDescription, + required String serviceID, + required String projectID, + Function(PaytabsTransactionResponseModel)? onSuccess, + Function(String)? onError, + }) async { bool _callbackFired = false; // prevent duplicate callback invocations - FlutterPaytabsBridge.startCardPayment(paymentConfiguration, (event) { - if (_callbackFired) return; // ignore any subsequent callback calls + try { + LoaderBottomSheet.showLoader(); - final transactionDetails = event["data"] as Map?; - if (event["status"] == "success") { - _callbackFired = true; - // Cast Map to Map - final data = Map.from(event["data"] as Map); - paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data); - if (paytabsTransactionResponseModel.isSuccess!) { - onSuccess!(paytabsTransactionResponseModel); - } else { - // Transaction was processed but failed - final reason = - transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error"; - final responseCode = transactionDetails?["responseCode"] ?? ""; - final errorMessage = - responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason"; - onError!(errorMessage); + // Create PayfortRequestInsertModel (same as Payfort implementation) + PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel(); + + payfortRequestInsertModel.commandType = "SALE"; + payfortRequestInsertModel.amount = num.parse(amount); + payfortRequestInsertModel.customerEmail = getIt.get().getAuthenticatedUser()!.emailAddress; + payfortRequestInsertModel.orderDescription = orderDescription; + payfortRequestInsertModel.serviceID = serviceID; + payfortRequestInsertModel.paymentOption = paymentOption; + payfortRequestInsertModel.clientRequestID = transactionID; + payfortRequestInsertModel.projectID = projectID; + payfortRequestInsertModel.fileNumber = getIt.get().getAuthenticatedUser()!.patientId.toString(); + payfortRequestInsertModel.customerName = "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}"; + payfortRequestInsertModel.currency = getIt.get().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + payfortRequestInsertModel.liveServiceID = liveServiceID; + payfortRequestInsertModel.latitude = getIt.get().userLat.toString(); + payfortRequestInsertModel.longitude = getIt.get().userLong.toString(); + payfortRequestInsertModel.merchantReference = transactionID; + payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + payfortRequestInsertModel.language = getIt.get().isArabic() ? "ar" : "en"; + payfortRequestInsertModel.integrationId = "2"; // PayTabs integration ID + payfortRequestInsertModel.isSchedule = isScheduleLiveCare; + payfortRequestInsertModel.appointmentDate = appointmentDate; + payfortRequestInsertModel.appointmentNo = appointmentNo; + payfortRequestInsertModel.doctorID = doctorID; + + // Call request insert API before payment + await paytabsRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel); + + LoaderBottomSheet.hideLoader(); + + // Start PayTabs payment + FlutterPaytabsBridge.startCardPayment(paymentConfiguration, (event) async { + if (_callbackFired) return; // ignore any subsequent callback calls + + final transactionDetails = event["data"] as Map?; + if (event["status"] == "success") { + _callbackFired = true; + + LoaderBottomSheet.showLoader(); + + // Cast Map to Map + final data = Map.from(event["data"] as Map); + paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data); + + // Create PayfortResponseInsertModel (same as Payfort implementation) + PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel(); + + payfortResponseInsertModel.fortId = paytabsTransactionResponseModel.transactionReference ?? ""; + payfortResponseInsertModel.commandType = paytabsTransactionResponseModel.transactionType ?? "SALE"; + // payfortResponseInsertModel.amount = paytabsTransactionResponseModel.tranTotal ?? 0; + payfortResponseInsertModel.amount = num.parse(amount); + payfortResponseInsertModel.sadadOLP = ""; + payfortResponseInsertModel.paymentOption = paytabsTransactionResponseModel.paymentInfo?.paymentMethod ?? ""; + payfortResponseInsertModel.customerIP = ""; + payfortResponseInsertModel.eCI = ""; + payfortResponseInsertModel.responseMessage = paytabsTransactionResponseModel.payResponseReturn ?? ""; + payfortResponseInsertModel.cardNumber = paytabsTransactionResponseModel.paymentInfo!.paymentDescription; + payfortResponseInsertModel.cardBin = ""; + payfortResponseInsertModel.installmentsNumber = 0; + payfortResponseInsertModel.status = paytabsTransactionResponseModel.isSuccess! ? "success" : "failed"; + payfortResponseInsertModel.merchantRef = transactionID; + payfortResponseInsertModel.isRefund = false; + payfortResponseInsertModel.remmeberMe = true; + payfortResponseInsertModel.reconciliationReference = ""; + + // Call response insert API after payment + await paytabsResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); + + LoaderBottomSheet.hideLoader(); + + if (paytabsTransactionResponseModel.isSuccess!) { + if (onSuccess != null) { + onSuccess(paytabsTransactionResponseModel); + } + } else { + // Transaction was processed but failed + final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error"; + final responseCode = transactionDetails?["responseCode"] ?? ""; + final errorMessage = responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason"; + if (onError != null) { + onError(errorMessage); + } + } + } else if (event["status"] == "error") { + _callbackFired = true; + final errorMessage = event["message"] ?? "An error occurred"; + debugPrint("Error occurred in transaction: $errorMessage"); + debugPrint("Full error event: $event"); + if (onError != null) { + onError(errorMessage); + } + } else if (event["status"] == "event") { + // Intermediate SDK events – do NOT mark _callbackFired, just log + final eventMessage = event["message"] ?? "Event occurred"; + debugPrint("Event occurred: $eventMessage"); } - } else if (event["status"] == "error") { - _callbackFired = true; - final errorMessage = event["message"] ?? "An error occurred"; - debugPrint("Error occurred in transaction: $errorMessage"); - debugPrint("Full error event: $event"); - onError!(errorMessage); - } else if (event["status"] == "event") { - // Intermediate SDK events – do NOT mark _callbackFired, just log - final eventMessage = event["message"] ?? "Event occurred"; - debugPrint("Event occurred: $eventMessage"); + }); + } catch (error) { + debugPrint("PayTabs Payment Error: $error"); + LoaderBottomSheet.hideLoader(); + if (onError != null) { + onError("Payment initialization failed: $error"); } - }); + } } startAlternatePayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) { diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index bfb83a2c..d75b3f8e 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -64,7 +64,7 @@ class PrescriptionsViewModel extends ChangeNotifier { PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices}); initPrescriptionsViewModel() { - // if (isPrescriptionsDataNeedsReloading) { + if (isPrescriptionsDataNeedsReloading) { patientPrescriptionOrders.clear(); patientPrescriptionOrdersByClinic.clear(); patientPrescriptionOrdersByHospital.clear(); @@ -72,7 +72,7 @@ class PrescriptionsViewModel extends ChangeNotifier { prescriptionsOrderList.clear(); isPrescriptionsOrdersLoading = true; getPatientPrescriptionOrders(); - // } + } isSortByClinic = true; isPrescriptionsDeliveryOrdersLoading = true; notifyListeners(); @@ -138,9 +138,9 @@ class PrescriptionsViewModel extends ChangeNotifier { } Future getPatientPrescriptionOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isPrescriptionsDataNeedsReloading) { - // return; - // } + if (!isPrescriptionsDataNeedsReloading) { + return; + } final result = await prescriptionsRepo.getPatientPrescriptionOrders(patientId: "1231755"); @@ -191,7 +191,9 @@ class PrescriptionsViewModel extends ChangeNotifier { result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { - onError!(failure.message); + if (onError != null) { + onError(failure.message); + } }, (apiResponse) async { if (apiResponse.messageStatus == 2) { @@ -235,11 +237,16 @@ class PrescriptionsViewModel extends ChangeNotifier { result.fold( (failure) async { - onError!(failure.message); + if (onError != null) { + onError(failure.message); + } }, (apiResponse) { if (apiResponse.messageStatus == 2) { - onError!(apiResponse.errorMessage!); + if (onError != null) { + onError(apiResponse.errorMessage ?? "An error occurred while fetching the prescription instructions PDF."); + } + // onError!(apiResponse.errorMessage!); // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { prescriptionInstructionsPDFLink = apiResponse.data; diff --git a/lib/features/profile_picture/profile_picture_view_model.dart b/lib/features/profile_picture/profile_picture_view_model.dart index 017ca5d8..59198809 100644 --- a/lib/features/profile_picture/profile_picture_view_model.dart +++ b/lib/features/profile_picture/profile_picture_view_model.dart @@ -70,9 +70,7 @@ class ProfilePictureViewModel extends ChangeNotifier { try { _cachedImageBytes = base64Decode(imageData); _cachedImageDataHash = '${imageData.length}_${imageData.hashCode}'; - print('✅ Cached existing profile image'); } catch (e) { - print('❌ Error caching existing image: $e'); _cachedImageBytes = null; _cachedImageDataHash = null; } @@ -85,7 +83,6 @@ class ProfilePictureViewModel extends ChangeNotifier { final currentPatientId = _appState.getAuthenticatedUser()?.patientId; if (currentPatientId != null && currentPatientId != _currentPatientId) { - print('🔄 User switched detected: $_currentPatientId -> $currentPatientId'); _handleUserSwitch(currentPatientId); return true; } @@ -97,8 +94,6 @@ class ProfilePictureViewModel extends ChangeNotifier { final oldPatientId = _currentPatientId; _currentPatientId = newPatientId; - print('🧹 Clearing cache for old user: $oldPatientId'); - // Clear AppState cache _appState.clearProfileImageCache(); @@ -113,17 +108,14 @@ class ProfilePictureViewModel extends ChangeNotifier { notifyListeners(); // Load new user's profile image - print('📥 Loading profile image for new user: $newPatientId'); _profileSettingsViewModel.getProfileImage( patientID: newPatientId, forceRefresh: true, onSuccess: (data) { - print('✅ Profile image loaded successfully for user: $newPatientId'); _tryCacheExistingImage(); notifyListeners(); }, onError: (error) { - print('❌ Error loading profile image: $error'); notifyListeners(); }, ); @@ -133,22 +125,18 @@ class ProfilePictureViewModel extends ChangeNotifier { void loadProfileImage({bool forceRefresh = false}) { // Check if profile image is already loaded in AppState (skip if forcing refresh) if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { - print('✅ Profile image already cached in AppState'); return; } final patientID = _appState.getAuthenticatedUser()?.patientId; if (patientID == null) { - print('⚠️ Cannot load profile image - no authenticated user'); return; } - print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)'); _profileSettingsViewModel.getProfileImage( patientID: patientID, forceRefresh: forceRefresh, onSuccess: (data) { - print('✅ Profile image loaded successfully'); _tryCacheExistingImage(); notifyListeners(); }, @@ -188,10 +176,6 @@ class ProfilePictureViewModel extends ChangeNotifier { false, // Don't show files option, only camera and gallery (base64String, file) async { try { - print('=== Starting image processing ==='); - print('File path: ${file.path}'); - print('File exists: ${await file.exists()}'); - print('Original file size: ${await file.length() / 1024} KB'); // Compress and resize the image print('Calling compressAndResizeImage...'); @@ -266,14 +250,10 @@ class ProfilePictureViewModel extends ChangeNotifier { onError('No authenticated user found'); return; } - - print('📤 Uploading profile image for patient: $patientID'); _profileSettingsViewModel.uploadProfileImage( patientID: patientID, imageData: base64String, onSuccess: (data) async { - print('✅ Profile image uploaded successfully'); - // Clear old cache first to ensure fresh data _cachedImageBytes = null; _cachedImageDataHash = null; @@ -287,9 +267,13 @@ class ProfilePictureViewModel extends ChangeNotifier { // Increment version to trigger targeted rebuild (no full screen refresh) _profileImageVersion.value++; - print('🔄 Profile image version updated to ${_profileImageVersion.value} (targeted rebuild)'); - onSuccess(data); + final String successMessage = data is String + ? data + : (data is Map && data['message'] != null + ? data['message'].toString() + : data?.toString() ?? 'Success'); + onSuccess(successMessage); }, onError: (error) { print('❌ Error uploading profile image: $error'); @@ -308,7 +292,6 @@ class ProfilePictureViewModel extends ChangeNotifier { try { _cachedImageBytes = base64Decode(imageData!); _cachedImageDataHash = currentHash; - print('🔄 Updated cached image bytes'); } catch (e) { print('❌ Error decoding profile image: $e'); _cachedImageBytes = null; @@ -326,7 +309,6 @@ class ProfilePictureViewModel extends ChangeNotifier { _cachedImageDataHash = null; _selectedImage = null; notifyListeners(); - print('🧹 Cleared all profile picture cache'); } /// Check if we should show shimmer loading diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 35342f84..1d945e1a 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1964,5 +1964,9 @@ abstract class LocaleKeys { static const doctorSchedule = 'doctorSchedule'; static const weeklyShift = 'weeklyShift'; static const dayOff = 'dayOff'; + static const hide = 'hide'; + static const viewPatientQR = 'viewPatientQR'; + static const patientQR = 'patientQR'; + static const forText = 'forText'; } diff --git a/lib/main.dart b/lib/main.dart index 6e3ef8cc..87746f14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,6 +63,7 @@ import 'core/utils/size_utils.dart'; import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; + @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -109,11 +110,6 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - // Download the latest JSONs from your server - // await TranslationService.downloadTranslations(); - - // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - try { // Attempt to get the default app. If it exists, this avoids the error. await Firebase.app(); @@ -374,6 +370,7 @@ class MyApp extends StatelessWidget { darkTheme: AppTheme.getDarkTheme(isArabic), themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light, navigatorKey: getIt.get().navigatorKey, + navigatorObservers: [getIt.get().routeObserver], ); }, ); diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 4d181230..a26995b1 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -1200,8 +1200,8 @@ class _AppointmentDetailsPageState extends State { patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, onSuccess: (apiResponse) { LoaderBottomSheet.hideLoader(); - myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - myAppointmentsViewModel.getPatientAppointments(true, false); + // myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + // myAppointmentsViewModel.getPatientAppointments(true, false); showCommonBottomSheet(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentConfirmedSuccessfully.tr(context: context)), callBackFunc: (str) { myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 46130b12..57b2c71d 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -69,12 +69,18 @@ class _AppointmentPaymentPageState extends State { widget.patientAppointmentHistoryResponseModel.clinicID, widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) { myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { - if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null) { + // if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) { + if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && + myAppointmentsViewModel.patientAppointmentShareResponseModel?.patientShareWithTax != null && + myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.minLimit?.amount != null && + myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.maxLimit?.amount != null) { if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { - setState(() { - isShowTamara = true; - }); + if (mounted) { + setState(() { + isShowTamara = true; + }); + } } } }); @@ -139,12 +145,30 @@ class _AppointmentPaymentPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() async { selectedPaymentMethod = "MADA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAppointmentTransID( + widget.patientAppointmentHistoryResponseModel.projectID, + widget.patientAppointmentHistoryResponseModel.clinicID, + widget.patientAppointmentHistoryResponseModel.appointmentNo, + ); paytabsViewModel.setPaymentConfiguration( "Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false", + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(), + transactionID: transID, + amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), + orderDescription: "Appointment Payment", + serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(), + projectID: widget.patientAppointmentHistoryResponseModel.projectID.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, projectID: widget.patientAppointmentHistoryResponseModel.projectID, @@ -362,12 +386,30 @@ class _AppointmentPaymentPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() async { selectedPaymentMethod = "VISA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAppointmentTransID( + widget.patientAppointmentHistoryResponseModel.projectID, + widget.patientAppointmentHistoryResponseModel.clinicID, + widget.patientAppointmentHistoryResponseModel.appointmentNo, + ); paytabsViewModel.setPaymentConfiguration( "Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false", + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(), + transactionID: transID, + amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), + orderDescription: "Appointment Payment", + serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(), + projectID: widget.patientAppointmentHistoryResponseModel.projectID.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, projectID: widget.patientAppointmentHistoryResponseModel.projectID, diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 1f54e3d1..da20e183 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -43,8 +43,11 @@ class _MyAppointmentsPageState extends State { @override void initState() { scheduleMicrotask(() { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + if (!myAppointmentsViewModel.isMyAppointmentsLoading) { + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); + } + // myAppointmentsViewModel.changeTabToArrived(); }); super.initState(); } @@ -67,9 +70,9 @@ class _MyAppointmentsPageState extends State { activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ - CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), + // CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), - CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)), + CustomTabBarModel(null, LocaleKeys.arrived.tr(context: context)), ], onTabChange: (index) { setState(() { @@ -78,6 +81,10 @@ class _MyAppointmentsPageState extends State { myAppointmentsViewModel.onTabChange(index); myAppointmentsViewModel.updateListWRTTab(index); context.read().flush(); + + if(index == 1) { + myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded(); + } }, ).paddingSymmetrical(24.h, 0.h), // Consumer(builder: (context, myAppointmentsVM, child) { @@ -148,7 +155,7 @@ class _MyAppointmentsPageState extends State { ? myAppointmentsVM.patientAppointmentsViewList.length : 1, itemBuilder: (context, index) { - final isExpanded = myAppointmentsVM.selectedTabIndex == 1 ? true : expandedIndex == index; + final isExpanded = myAppointmentsVM.selectedTabIndex == 0 ? true : expandedIndex == index; return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 9ccf471e..d05270e5 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -39,7 +39,7 @@ class _MyDoctorsPageState extends State { @override void initState() { scheduleMicrotask(() { - myAppointmentsViewModel.getPatientMyDoctors(); + myAppointmentsViewModel.getPatientMyDoctors(isTop8: false); }); super.initState(); } diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 39251cd6..1cbe3be2 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -14,6 +14,7 @@ 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/book_appointments/models/resp_models/get_clinic_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'; @@ -231,7 +232,7 @@ class _AppointmentCardState extends State { Utils.buildImgWithNetwork( url: widget.isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' - : widget.patientAppointmentHistoryResponseModel.doctorImageURL!, + : widget.patientAppointmentHistoryResponseModel.doctorImageURL, iconColor: AppColors.transparent, width: 63.h, height: 63.h, @@ -277,7 +278,7 @@ class _AppointmentCardState extends State { (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), Expanded( child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(30)}").toText16( - isBold: true, maxlines: 2, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + isBold: true, maxlines: 1, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), ), SizedBox(width: 12.w), (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && @@ -677,7 +678,15 @@ class _AppointmentCardState extends State { projectName: widget.patientAppointmentHistoryResponseModel.projectName, ); + final clinic = GetClinicsListResponseModel( + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName, + clinicDescriptionN: widget.patientAppointmentHistoryResponseModel.clinicName, + ); + widget.bookAppointmentsViewModel.setSelectedDoctor(doctor); + widget.bookAppointmentsViewModel.setSelectedClinic(clinic); + LoaderBottomSheet.showLoader(); await widget.bookAppointmentsViewModel.getDoctorFreeSlots( diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index f4d20944..6e3f77d1 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -101,7 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget { Row( children: [ Expanded( - child: patientAppointmentHistoryResponseModel.doctorNameObj!.toText16( + child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16( isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), textOverflow: TextOverflow.ellipsis, @@ -127,9 +127,9 @@ class AppointmentDoctorCard extends StatelessWidget { runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: (patientAppointmentHistoryResponseModel.clinicName!.length > 20 - ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' - : patientAppointmentHistoryResponseModel.clinicName!), + labelText: ((patientAppointmentHistoryResponseModel.clinicName ?? "").length > 20 + ? '${(patientAppointmentHistoryResponseModel.clinicName ?? "").substring(0, 20)}...' + : (patientAppointmentHistoryResponseModel.clinicName ?? "")), labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), ), AppCustomChipWidget( diff --git a/lib/presentation/appointments/widgets/doctor_row_appointment_rating.dart b/lib/presentation/appointments/widgets/doctor_row_appointment_rating.dart index df15173c..0cc94e7f 100644 --- a/lib/presentation/appointments/widgets/doctor_row_appointment_rating.dart +++ b/lib/presentation/appointments/widgets/doctor_row_appointment_rating.dart @@ -26,7 +26,7 @@ class BuildDoctorRowAppointmentRating extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: patientAppointmentHistoryResponseModel.doctorImageURL!, + url: patientAppointmentHistoryResponseModel.doctorImageURL, iconColor: AppColors.transparent, width: 63.h, height: 63.h, 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 b689162c..3a74306e 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem; 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:lottie/lottie.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -117,6 +118,14 @@ class _RegionBottomSheetBodyState extends State { height: MediaQuery.of(context).size.height * 0.5, child: Column( children: [ + // AppCustomChipWidget( + // richText: Row( + // children: [ + // "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isBold: true), + // LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor, isBold: true), + // ], + // ), + // ), // Consumer( // builder: (context, regionVM, _) { // return Padding( diff --git a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart index 6eae8870..e7d7c187 100644 --- a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart +++ b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart @@ -88,7 +88,7 @@ class AskDoctorAppointmentCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: askDoctorAppointmentHistoryList.doctorImageURL!, + url: askDoctorAppointmentHistoryList.doctorImageURL, iconColor: AppColors.blackBgColor, width: 63.h, height: 63.h, diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart index 8d44f377..1451ecb3 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -17,6 +17,9 @@ 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' show CustomButton; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.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/models/calendar_design_type.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'; @@ -153,30 +156,56 @@ class _RegisterNew extends State { }, ).withVerticalPadding(8), Divider(height: 1), - TextInputWidget( - labelText: LocaleKeys.dob.tr(context: context), - hintText: "11 July, 1994", - controller: authVm.dobController, - focusNode: _dobFocusNode, - isEnable: true, - prefix: null, - isAllowRadius: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - padding: EdgeInsets.symmetric(vertical: 8.h), - leadingIcon: AppAssets.birthday_cake, - selectionType: SelectionTypeEnum.calendar, - onCalendarTypeChanged: authVm.onCalenderTypeChange, - onChange: (value) { - authVm.onDobChange(value); - // Clear error when user selects a date - authVm.clearDobError(); + GestureDetector( + onTap: () async { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.selectDate.tr(context: context), + context, + child: DateRangeSelector( + onRangeSelected: (DateTime? start, DateTime? end) { + if (start != null) { + var picked = start.toString().split(" ").first; + // authVm.dobController.text = picked; + authVm.onDobChange(picked); + // Clear error when user selects a date + authVm.clearDobError(); + } + }, + designType: CalendarDesignType.designV2, + showCalendarToggle: true, + onCalendarTypeChanged: null, + btnTitle: LocaleKeys.done.tr(context: context), + ), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); }, - fontFamily: "Poppins", - hasError: false, - // Don't show individual field border - errorMessage: authVm.dobError, // Show error message if exists - ).withVerticalPadding(8), + child: TextInputWidget( + labelText: LocaleKeys.dob.tr(context: context), + hintText: "11 July, 1994", + controller: authVm.dobController, + focusNode: _dobFocusNode, + isEnable: false, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + selectionType: SelectionTypeEnum.calendar, + onCalendarTypeChanged: authVm.onCalenderTypeChange, + onChange: (value) { + authVm.onDobChange(value); + // Clear error when user selects a date + authVm.clearDobError(); + }, + fontFamily: "Poppins", + hasError: false, + // Don't show individual field border + errorMessage: authVm.dobError, // Show error message if exists + ).withVerticalPadding(8), + ), ], ), ), diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 19080ae8..bea93d36 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/features/authentication/authentication_view_ 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/home/navigation_screen.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/app_bar_widget.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; @@ -30,11 +31,24 @@ class SavedLogin extends StatefulWidget { _SavedLogin createState() => _SavedLogin(); } -class _SavedLogin extends State { +class _SavedLogin extends State with RouteAware { LoginTypeEnum loginType = LoginTypeEnum.sms; late AuthenticationViewModel authVm; late AppState appState; bool? isOther; + PageRoute? _pageRoute; + + void _assignSavedLoginData() { + authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!.startsWith("0") + ? appState.getSelectDeviceByImeiRespModelElement!.mobile!.replaceFirst("0", "") + : appState.getSelectDeviceByImeiRespModelElement!.mobile!; + authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!; + + if (isOther == true) { + authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!; + authVm.selectedCountrySignup = CountryEnum.others; + } + } @override void initState() { @@ -43,19 +57,10 @@ class _SavedLogin extends State { appState = getIt.get(); loginType = LoginTypeExtension.fromValue(appState.getSelectDeviceByImeiRespModelElement!.logInType!)!; - authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!.startsWith("0") - ? appState.getSelectDeviceByImeiRespModelElement!.mobile!.replaceFirst("0", "") - : appState.getSelectDeviceByImeiRespModelElement!.mobile!; - authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!; - // Check if this is an "Others" country user isOther = appState.getSelectDeviceByImeiRespModelElement!.isOther == true; - // Set country to Others if isOther flag is true - this ensures _Others APIs are called - if (isOther!) { - authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!; - authVm.selectedCountrySignup = CountryEnum.others; - } + _assignSavedLoginData(); if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { authVm.loginWithFingerPrintFace(() {}); @@ -63,6 +68,30 @@ class _SavedLogin extends State { super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final route = ModalRoute.of(context); + if (route is PageRoute && route != _pageRoute) { + if (_pageRoute != null) { + getIt.get().routeObserver.unsubscribe(this); + } + _pageRoute = route; + getIt.get().routeObserver.subscribe(this, route); + } + } + + @override + void didPopNext() { + _assignSavedLoginData(); + } + + @override + void dispose() { + getIt.get().routeObserver.unsubscribe(this); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index d542bb7f..c8fcdb36 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -66,14 +66,14 @@ class _BookAppointmentPageState extends State { // bookAppointmentsViewModel.getLocation(); immediateLiveCareViewModel.initImmediateLiveCare(); if (appState.isAuthenticated) { - getIt.get().getPatientMyDoctors(); - getIt.get().getPatientFavouriteDoctors(); + getIt.get().getPatientMyDoctors(isTop8: true); + // getIt.get().getPatientFavouriteDoctors(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (bookAppointmentsViewModel.selectedTabIndex == 1) { if (appState.isAuthenticated) { - getIt.get().getPatientMyDoctors(); + getIt.get().getPatientMyDoctors(isTop8: true); getIt.get().getPatientFavouriteDoctors(); showUnKnownClinicBottomSheet(); } @@ -166,6 +166,7 @@ class _BookAppointmentPageState extends State { Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), ], ).paddingSymmetrical(24.h, 0.h).onPress(() { + myAppointmentsVM.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), @@ -195,7 +196,7 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL, iconColor: AppColors.transparent, width: 64.h, height: 64.h, @@ -388,7 +389,7 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, + url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl, iconColor: AppColors.transparent, width: 64.h, height: 64.h, @@ -551,6 +552,7 @@ class _BookAppointmentPageState extends State { Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { + Clarity.sendCustomEvent('Select_Region_Page'); bookAppointmentsViewModel.setProjectID(null); openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); }), diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 6be110ec..5b6ad09d 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -1,3 +1,4 @@ +import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -8,6 +9,7 @@ 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/get_clinic_list_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/book_appointment/widgets/appointment_calendar.dart'; @@ -29,6 +31,7 @@ class DoctorProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { + Clarity.setCurrentScreenName('Doctor Profile Page'); final bookAppointmentsViewModel = Provider.of(context, listen: false); final appState = getIt.get(); return Scaffold( @@ -86,7 +89,7 @@ class DoctorProfilePage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!, + url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL, iconColor: AppColors.transparent, width: 63.h, height: 63.h, @@ -113,7 +116,7 @@ class DoctorProfilePage extends StatelessWidget { ], ), Utils.buildImgWithNetwork( - url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!, + url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL, iconColor: AppColors.transparent, width: 32.h, height: 32.h, @@ -240,6 +243,14 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription; bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName; + final clinic = GetClinicsListResponseModel( + clinicID: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicID, + clinicDescription: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription, + clinicDescriptionN: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription, + ); + + bookAppointmentsViewModel.setSelectedClinic(clinic); + LoaderBottomSheet.showLoader(); bookAppointmentsViewModel.isLiveCareSchedule ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( 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 972606a9..9ef3f8fb 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -130,11 +130,29 @@ class _ImmediateLiveCarePaymentPageState extends State { void initiateBookAppointment() async { // LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); + Clarity.sendCustomEvent('Appointment_book_success'); LoaderBottomSheet.showLoader( loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) @@ -449,6 +450,7 @@ class _ReviewAppointmentPageState extends State { callBackFunc: () { bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); bookAppointmentsViewModel.setIsLiveCareSchedule(false); + bookAppointmentsViewModel.setSelectedChiefComplaintID(0); Navigator.pushAndRemoveUntil( context, CustomPageRoute( @@ -524,6 +526,7 @@ class _ReviewAppointmentPageState extends State { child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () { bookAppointmentsViewModel.setIsLiveCareSchedule(false); bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); + bookAppointmentsViewModel.setSelectedChiefComplaintID(0); Navigator.pushAndRemoveUntil(context, CustomPageRoute(page: LandingNavigation()), (r) => false); }, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true); }); diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index cf1cd897..bf4b5d58 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1021,6 +1021,7 @@ class _SelectClinicPageState extends State { void onClinicSelected(GetClinicsListResponseModel clinic) { bookAppointmentsViewModel.setSelectedClinic(clinic); bookAppointmentsViewModel.setIsDoctorsListLoading(true); + searchEditingController.text = ""; if (clinic.isLiveCareClinicAndOnline ?? false) { Navigator.of(context).push( CustomPageRoute( @@ -1202,7 +1203,9 @@ class _SelectClinicPageState extends State { getTitle(AppointmentViaRegionViewmodel data) { if (data.selectedRegionId == null) { - return LocaleKeys.selectRegion.tr(context: context).toText20(isBold: true); + return Expanded( + child: "${LocaleKeys.selectRegion.tr(context: context)} ${LocaleKeys.forText.tr(context: context)} '${bookAppointmentsViewModel.selectedClinic.clinicDescription}'".toText20(isBold: true), + ); } else { return Transform.flip( flipX: data.isArabic ? true : false, diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 26ab5519..cad4fe0e 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -48,7 +48,7 @@ class _SelectDoctorPageState extends State { Clarity.setCurrentScreenName('Select Doctor Page'); _scrollController = ScrollController(); scheduleMicrotask(() { - bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); + bookAppointmentsViewModel.setIsNearestAppointmentSelected(false); if (bookAppointmentsViewModel.isLiveCareSchedule) { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { @@ -57,7 +57,7 @@ class _SelectDoctorPageState extends State { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { - bookAppointmentsViewModel.getDoctorsList(); + bookAppointmentsViewModel.getDoctorsList(isNearest: false); } } }); @@ -133,8 +133,28 @@ class _SelectDoctorPageState extends State { isLoading: bookAppointmentsVM.isClinicsListLoading, doctorsListResponseModel: bookAppointmentsVM.liveCareDoctorsList[index], bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() { - // onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.liveCareDoctorsList[index]); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage( + isDoctorAllowedToBook: true, + ), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); }), ), ), 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 index c6e17660..c33d950e 100644 --- a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart @@ -138,12 +138,30 @@ class _WaitingAppointmentPaymentPageState extends State { // ), SizedBox( height: MediaQuery.of(context).size.height * 0.45, - child: Directionality( - textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr, - child: Localizations.override( - context: context, - locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'), - child: ClarityUnmask( - child: SfCalendar( - controller: _calendarController, - minDate: DateTime.now(), - showNavigationArrow: true, - // headerHeight: 60.h, - headerStyle: CalendarHeaderStyle( - backgroundColor: AppColors.transparent, - textAlign: isArabic ? TextAlign.end : TextAlign.start, - textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor), - ), - viewHeaderStyle: ViewHeaderStyle( - // backgroundColor: AppColors.scaffoldBgColor, - dayTextStyle: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor), - ), - view: CalendarView.month, - todayHighlightColor: Colors.transparent, - todayTextStyle: TextStyle(color: AppColors.textColor, fontWeight: FontWeight.bold), - selectionDecoration: ShapeDecoration( - color: AppColors.transparent, - shape: SmoothRectangleBorder( - borderRadius: BorderRadius.circular(10.r), - smoothness: 1, - side: BorderSide(color: AppColors.primaryRedColor, width: 1.5), + child: Stack( + children: [ + Directionality( + textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr, + child: Localizations.override( + context: context, + locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'), + child: ClarityUnmask( + child: SfCalendar( + controller: _calendarController, + minDate: DateTime.now(), + initialDisplayDate: DateUtil.convertStringToDate(bookAppointmentsViewModel.freeSlotsResponse[0]), + showNavigationArrow: false, + headerHeight: 60.h, + headerStyle: CalendarHeaderStyle( + backgroundColor: AppColors.transparent, + // textAlign: isArabic ? TextAlign.end : TextAlign.start, + textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor), + ), + viewHeaderStyle: ViewHeaderStyle( + // backgroundColor: AppColors.scaffoldBgColor, + dayTextStyle: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor), + ), + view: CalendarView.month, + todayHighlightColor: Colors.transparent, + todayTextStyle: TextStyle(color: AppColors.textColor, fontWeight: FontWeight.bold), + selectionDecoration: ShapeDecoration( + color: AppColors.transparent, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + smoothness: 1, + side: BorderSide(color: AppColors.primaryRedColor, width: 1.5), + ), + ), + cellBorderColor: AppColors.transparent, + dataSource: MeetingDataSource(_getDataSource()), + monthCellBuilder: (context, details) => Padding( + padding: EdgeInsets.all(12.h), + child: details.date.day.toString().toText14( + isCenter: true, + color: details.date == _calendarController.selectedDate ? AppColors.primaryRedColor : AppColors.textColor, + isEnglishOnly: true, + ), + ), + monthViewSettings: MonthViewSettings( + dayFormat: "EEE", + appointmentDisplayMode: MonthAppointmentDisplayMode.indicator, + showTrailingAndLeadingDates: false, + appointmentDisplayCount: 1, + monthCellStyle: MonthCellStyle( + textStyle: TextStyle(fontSize: 19.f), + ), + ), + onTap: (CalendarTapDetails details) { + _calendarController.selectedDate = details.date; + _onDaySelected(details.date!); + }, ), ), - cellBorderColor: AppColors.transparent, - dataSource: MeetingDataSource(_getDataSource()), - monthCellBuilder: (context, details) => Padding( - padding: EdgeInsets.all(12.h), - child: details.date.day.toString().toText14( - isCenter: true, - color: details.date == _calendarController.selectedDate ? AppColors.primaryRedColor : AppColors.textColor, - isEnglishOnly: true, + ), + ), + // Custom Navigation Arrows + Positioned( + top: 0, + right: isArabic ? null : 0, + left: isArabic ? 0 : null, + child: SizedBox( + height: 60.h, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Previous Month Arrow + GestureDetector( + onTap: () { + setState(() { + _calendarController.backward!(); + }); + }, + child: Container( + padding: EdgeInsets.all(8.h), + child: Icon( + // isArabic ? Icons.arrow_forward_ios : Icons.arrow_back_ios, + Icons.arrow_back_ios, + size: 24.h, + color: AppColors.primaryRedColor, + ), ), + ), + // Next Month Arrow + GestureDetector( + onTap: () { + setState(() { + _calendarController.forward!(); + }); + }, + child: Container( + padding: EdgeInsets.all(8.h), + child: Icon( + // isArabic ? Icons.arrow_back_ios : Icons.arrow_forward_ios, + Icons.arrow_forward_ios, + size: 24.h, + color: AppColors.primaryRedColor, + ), + ), + ), + ], ), - monthViewSettings: MonthViewSettings( - dayFormat: "EEE", - appointmentDisplayMode: MonthAppointmentDisplayMode.indicator, - showTrailingAndLeadingDates: false, - appointmentDisplayCount: 1, - monthCellStyle: MonthCellStyle( - textStyle: TextStyle(fontSize: 19.f), - ), - ), - onTap: (CalendarTapDetails details) { - _calendarController.selectedDate = details.date; - _onDaySelected(details.date!); - }, ), ), - ), + ], ), ), SizedBox(height: 10.h), @@ -164,7 +219,7 @@ class _AppointmentCalendarState extends State { ? ClarityUnmask( child: ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.15, + maxHeight: MediaQuery.of(context).size.height * 0.25, minHeight: 0, ), child: Directionality( @@ -213,15 +268,31 @@ class _AppointmentCalendarState extends State { ), ); } else { - bookAppointmentsViewModel.getAppointmentNearestGate( - projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); - bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay); - Navigator.of(context).pop(); - Navigator.of(context).push( - CustomPageRoute( - page: ReviewAppointmentPage(), - ), - ); + if (bookAppointmentsViewModel.selectedClinic.clinicID == 17 && bookAppointmentsViewModel.selectedChiefComplaintID == 0) { + GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( + clinicID: bookAppointmentsViewModel.selectedClinic.clinicID, + clinicDescription: bookAppointmentsViewModel.selectedClinic.clinicDescription, + isLiveCareClinicAndOnline: false, + liveCareServiceID: 0, + liveCareClinicID: 0); + bookAppointmentsViewModel.setProjectID(bookAppointmentsViewModel.selectedDoctor.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); + bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay); + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: ReviewAppointmentPage(), + ), + ); + } } } else { showCommonBottomSheetWithoutHeight( @@ -231,8 +302,7 @@ class _AppointmentCalendarState extends State { 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), (LocaleKeys.loginToUseService.tr(context: context)).toText16(color: AppColors.blackColor), SizedBox(height: 16.h), @@ -331,12 +401,7 @@ class _AppointmentCalendarState extends State { selectedButtonIndex = 0; List> timeList = []; for (var i = 0; i < dayEvents.length; i++) { - Map timeSlot = { - "isoTime": dayEvents[i].isoTime, - "start": dayEvents[i].start.toString(), - "end": dayEvents[i].end.toString(), - "vidaDate": dayEvents[i].vidaDate - }; + Map timeSlot = {"isoTime": dayEvents[i].isoTime, "start": dayEvents[i].start.toString(), "end": dayEvents[i].end.toString(), "vidaDate": dayEvents[i].vidaDate}; timeList.add(timeSlot); } if (dayEvents.isNotEmpty) { diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index fe7cf3e0..bb5a599d 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -144,8 +144,9 @@ class DoctorCard extends StatelessWidget { spacing: 3.h, runSpacing: 4.h, children: [ - bookAppointmentsViewModel.isNearestAppointmentSelected - ? doctorsListResponseModel.nearestFreeSlot != null + // bookAppointmentsViewModel.isNearestAppointmentSelected + // ? + doctorsListResponseModel.nearestFreeSlot != null ? AppCustomChipWidget( labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)), // richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)) @@ -155,7 +156,8 @@ class DoctorCard extends StatelessWidget { textColor: AppColors.successColor, ).toShimmer2(isShow: isLoading) : SizedBox.shrink() - : SizedBox.shrink(), + // : SizedBox.shrink() + , AppCustomChipWidget( labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}", ).toShimmer2(isShow: isLoading), @@ -167,9 +169,10 @@ class DoctorCard extends StatelessWidget { ], ), ), - (doctorsListResponseModel.isLiveCare ?? true) - ? SizedBox.shrink() - : Expanded( + // (doctorsListResponseModel.isLiveCare ?? true) + // ? SizedBox.shrink() + // : + Expanded( flex: 1, child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).toShimmer2(isShow: isLoading), ), diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index f3579cda..91fa3b52 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -260,7 +260,7 @@ class FeedbackPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!, + url: contactUsViewModel.patientFeedbackSelectedAppointment?.doctorImageURL??"", iconColor: AppColors.transparent, width: 63.h, height: 63.h, @@ -354,10 +354,10 @@ class FeedbackPage extends StatelessWidget { ImageOptions.showImageOptionsNew( context, true, - (String image, file) { - - // Navigator.pop(context); - contactUsViewModel.addFeedbackAttachment(image); + (String base64Image, file) { + // base64Image: The base64-encoded image string + // file: The File object + contactUsViewModel.addFeedbackAttachment(base64Image, file); }, ); }, @@ -384,25 +384,31 @@ class FeedbackPage extends StatelessWidget { child: ListView.builder( padding: EdgeInsets.all(16.h), shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), itemCount: contactUsViewModel.feedbackAttachmentList.length, itemBuilder: (BuildContext context, int index) { + final attachment = contactUsViewModel.feedbackAttachmentList[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), - ], + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + ), + SizedBox(width: 8.w), + Expanded( + child: attachment.fileName.toText14().paddingOnly(bottom: 8.h), + ), + ], + ), ), Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon).onPress(() { - contactUsViewModel.removeFeedbackAttachment(contactUsViewModel.feedbackAttachmentList[index]); + contactUsViewModel.removeFeedbackAttachment(attachment); }), ], ); 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 index 64856226..eb3fc2c7 100644 --- 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 @@ -124,12 +124,29 @@ class _ErOnlineCheckinPaymentPageState extends State ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "MADA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID( + emergencyServicesViewModel.selectedHospital!.iD, + appState.getAuthenticatedUser()!.patientId!, + ); paytabsViewModel.setPaymentConfiguration( "ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - if (emergencyServicesViewModel.isERBookAppointment) { + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "0", + appointmentDate: "", + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), + orderDescription: "ER Online Check-In Payment", + serviceID: ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum().toString(), + projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + if (emergencyServicesViewModel.isERBookAppointment) { await emergencyServicesViewModel.ER_CreateAdvancePayment( paymentMethodName: selectedPaymentMethod, paymentReference: transactionData.transactionReference!, @@ -214,12 +231,29 @@ class _ErOnlineCheckinPaymentPageState extends State ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "VISA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID( + emergencyServicesViewModel.selectedHospital!.iD, + appState.getAuthenticatedUser()!.patientId!, + ); paytabsViewModel.setPaymentConfiguration( "ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - if (emergencyServicesViewModel.isERBookAppointment) { + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "0", + appointmentDate: "", + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), + orderDescription: "ER Online Check-In Payment", + serviceID: ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum().toString(), + projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + if (emergencyServicesViewModel.isERBookAppointment) { await emergencyServicesViewModel.ER_CreateAdvancePayment( paymentMethodName: selectedPaymentMethod, paymentReference: transactionData.transactionReference!, diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 36a42ff2..eb938d17 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -89,7 +89,7 @@ class _HabibWalletState extends State { "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}" .toText19(isBold: true, color: Colors.white), "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId!}" - .toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true), + .toText14(isBold: true, color: AppColors.greyTextColor), ], ).expanded, Utils.buildSvgWithAssets(icon: AppAssets.habiblogo, width: 24.h, height: 24.h, applyThemeColor: false), diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index f894c387..537a7dd8 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -113,9 +113,23 @@ class _WalletPaymentConfirmPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() async { selectedPaymentMethod = "MADA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber)); paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - LoaderBottomSheet.showLoader(); + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "false", + appointmentDate: DateUtil.convertDateToString(DateTime.now()), + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: habibWalletVM.walletRechargeAmount.toString(), + fileNumber: habibWalletVM.fileNumber.toString(), + orderDescription: "Advance Payment", + serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(), + projectID: habibWalletVM.selectedHospital!.iD!.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + LoaderBottomSheet.showLoader(); await habibWalletVM.HISCreateAdvancePayment( paymentMethodName: selectedPaymentMethod, paidAmount: habibWalletVM.walletRechargeAmount, @@ -162,6 +176,7 @@ class _WalletPaymentConfirmPageState extends State { ); }); }, onError: (err) { + LoaderBottomSheet.showLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: err.toString()), @@ -295,9 +310,23 @@ class _WalletPaymentConfirmPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() async { selectedPaymentMethod = "VISA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber)); paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - LoaderBottomSheet.showLoader(); + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "false", + appointmentDate: DateUtil.convertDateToString(DateTime.now()), + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: habibWalletVM.walletRechargeAmount.toString(), + fileNumber: habibWalletVM.fileNumber.toString(), + orderDescription: "Advance Payment", + serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(), + projectID: habibWalletVM.selectedHospital!.iD!.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + LoaderBottomSheet.showLoader(); await habibWalletVM.HISCreateAdvancePayment( paymentMethodName: selectedPaymentMethod, paidAmount: habibWalletVM.walletRechargeAmount, diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 75144cf7..582fd6fc 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -1,6 +1,7 @@ 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/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; @@ -13,8 +14,10 @@ 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/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/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/hospital/hospital_selection_view_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/features/weather/weather_view_model.dart'; @@ -45,6 +48,7 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/dependencies.dart'; +import '../emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; class ServicesPage extends StatefulWidget { final bool showBackIcon; @@ -116,51 +120,51 @@ class _ServicesPageState extends State { ); } }), - // HmgServicesComponentModel( - // 11, - // LocaleKeys.indoorNavigation.tr(), - // "", - // AppAssets.indoor_nav_icon, - // bgColor: Color(0xff45A2F8), - // true, - // route: null, - // onTap: () { - // showCommonBottomSheetWithoutHeight( - // title: LocaleKeys.selectHospital.tr(), - // GetIt.instance().navigatorKey.currentContext!, - // child: ChangeNotifierProvider( - // create: (context) => HospitalSelectionBottomSheetViewModel(getIt()), - // child: Consumer( - // builder: (_, vm, __) => HospitalBottomSheetBody( - // searchText: vm.searchController, - // displayList: vm.displayList, - // onFacilityClicked: (value) { - // vm.setSelectedFacility(value); - // vm.getDisplayList(); - // }, - // onHospitalClicked: (hospital) { - // Navigator.pop(GetIt.instance().navigatorKey.currentContext!); - // vm.openPenguin(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: () { - // GetIt.instance().navigatorKey.currentContext!.read().clearSearchText(); - // }, - // ); - // }, - // ), + HmgServicesComponentModel( + 11, + LocaleKeys.indoorNavigation.tr(), + "", + AppAssets.indoor_nav_icon, + bgColor: Color(0xff45A2F8), + true, + route: null, + onTap: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.selectHospital.tr(), + GetIt.instance().navigatorKey.currentContext!, + child: ChangeNotifierProvider( + create: (context) => HospitalSelectionBottomSheetViewModel(getIt()), + child: Consumer( + builder: (_, vm, __) => HospitalBottomSheetBody( + searchText: vm.searchController, + displayList: vm.displayList, + onFacilityClicked: (value) { + vm.setSelectedFacility(value); + vm.getDisplayList(); + }, + onHospitalClicked: (hospital) { + Navigator.pop(GetIt.instance().navigatorKey.currentContext!); + vm.openPenguin(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: () { + GetIt.instance().navigatorKey.currentContext!.read().clearSearchText(); + }, + ); + }, + ), HmgServicesComponentModel( 11, LocaleKeys.eReferralServices.tr(), "", AppAssets.eReferral, bgColor: AppColors.eReferralCardColor, true, route: null, onTap: () async { if (getIt.get().isAuthenticated) { diff --git a/lib/presentation/home/lakum_wallet_details.dart b/lib/presentation/home/lakum_wallet_details.dart index d2ef15f4..bcd5cb3e 100644 --- a/lib/presentation/home/lakum_wallet_details.dart +++ b/lib/presentation/home/lakum_wallet_details.dart @@ -17,6 +17,7 @@ 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'; import 'package:url_launcher/url_launcher.dart'; +import 'dart:ui' as ui; class LakumWalletDetails extends StatelessWidget { const LakumWalletDetails({super.key}); @@ -239,10 +240,10 @@ class LakumWalletDetails extends StatelessWidget { return CustomButton( height: 46.h, icon: AppAssets.redeem_icon, - iconColor: AppColors.whiteColor, + iconColor: AppColors.textColor, iconSize: 24.h, backgroundColor: AppColors.habibPharmacyColor, - textColor: AppColors.whiteColor, + textColor: AppColors.textColor, text: LocaleKeys.redeem.tr(context: context), borderWidth: 0.w, fontWeight: FontWeight.w600, @@ -350,7 +351,10 @@ class LakumWalletDetails extends StatelessWidget { maxlines: 2, ), SizedBox(height: 2.h), - formattedDate.toText12(color: AppColors.greyTextColor), + Directionality( + textDirection: ui.TextDirection.ltr, + child: formattedDate.toText12(color: AppColors.greyTextColor), + ), ], ), ), @@ -363,10 +367,10 @@ class LakumWalletDetails extends StatelessWidget { isBold: true, color: transactionColor, ), - if (transaction.amount != null && transaction.amount! > 0) - "${transaction.amount?.toStringAsFixed(2) ?? "0.00"} SAR".toText11( - color: AppColors.greyTextColor, - ), + // if (transaction.amount != null && transaction.amount! > 0) + // "${transaction.amount?.toStringAsFixed(2) ?? "0.00"} SAR".toText11( + // color: AppColors.greyTextColor, + // ), ], ), ], diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 85f1e972..3caed75a 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -120,12 +120,23 @@ class _LandingPageState extends State { authVM = context.read(); habibWalletVM = context.read(); appointmentRatingViewModel = context.read(); + appState = getIt.get(); authVM.savePushTokenToAppState(); if (mounted) { - authVM.checkLastLoginStatus(() { - showQuickLogin(context); - }); + final user = appState.getAuthenticatedUser(); + final hasUserQuickLoginData = + user != null && + (user.mobileNumber?.isNotEmpty ?? false) && + (user.patientIdentificationNo?.isNotEmpty ?? false); + + if (hasUserQuickLoginData) { + authVM.checkLastLoginStatus(() { + if (mounted) { + showQuickLogin(context); + } + }); + } } _scrollController.addListener(() { @@ -154,7 +165,7 @@ class _LandingPageState extends State { immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); emergencyServicesViewModel.checkPatientERAdvanceBalance(); // myAppointmentsViewModel.getPatientAppointmentQueueDetails(); notificationsViewModel.initNotificationsViewModel(); @@ -172,12 +183,19 @@ class _LandingPageState extends State { appointmentNo, projectID, onSuccess: ((response) { + // Only open dialog if details are loaded AND widget is mounted + if (!mounted || appointmentRatingViewModel.appointmentDetails == null) return; + appointmentRatingViewModel.setClinicOrDoctor(false); - appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context)); - appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); + appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr()); + appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr()); openLastRating(); appState.setRatedVisible(true); }), + onError: (String errorMessage) { + // Silently mark as rated to prevent retry loops + if (mounted) appState.setRatedVisible(true); + }, ); } }, @@ -212,7 +230,7 @@ class _LandingPageState extends State { // Refresh Appointments Data myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); // Refresh Appointments Data habibWalletVM.initHabibWalletProvider(); @@ -253,8 +271,8 @@ class _LandingPageState extends State { padding: EdgeInsets.only( top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) ? (MediaQuery.paddingOf(context).top + 70.h) - : kToolbarHeight + 0.h, - bottom: 24), + : kToolbarHeight + 16.h, + bottom: 24.h), child: Column( spacing: 16.h, children: [ @@ -493,8 +511,8 @@ class _LandingPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() { myAppointmentsViewModel.onTabChange(0); myAppointmentsViewModel.updateListWRTTab(0); - Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); + Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); // getIt.get().setPaymentConfiguration("Appointment Payment", 15.5); // getIt.get().startCardPayment(); }), @@ -1458,6 +1476,7 @@ class _LandingPageState extends State { } openLastRating() { + if (!mounted) return; showCommonBottomSheetWithoutHeight( context, titleWidget: Selector( diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index 7c037604..a0d9553d 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -128,7 +128,9 @@ class LargeServiceCard extends StatelessWidget { ), ], ), - ); + ).onPress(() { + handleOnTap(); + }); } void handleOnTap() { @@ -275,7 +277,9 @@ class FadedLargeServiceCard extends StatelessWidget { ), ], ), - ); + ).onPress(() { + handleOnTap(); + }); } void handleOnTap() { diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 039da8c1..e3d96e3f 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -41,9 +41,13 @@ class WelcomeWidget extends StatelessWidget { borderRadius: BorderRadius.circular(30), child: Row( mainAxisSize: MainAxisSize.min, - spacing: 8.h, + // spacing: 8.h, children: [ - Icon(Icons.menu, color: AppColors.textColor).onPress(() { + SizedBox( + height: 40.h, + width: 20.w, + child: Icon(Icons.menu, color: AppColors.textColor), + ).onPress(() { getIt.get().getProfileSettings(); Navigator.of(context).push( CustomPageRoute( @@ -92,7 +96,7 @@ class WelcomeWidget extends StatelessWidget { ], ), ], - ).expanded, + ).paddingOnly(left: 8.h, right: 8.h).expanded, ], ), ), diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index b1fce84a..1a5fe039 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -84,7 +84,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: insuranceApprovalResponseModel.doctorImageURL!, + url: insuranceApprovalResponseModel.doctorImageURL, iconColor: AppColors.blackBgColor, width: 63.h, height: 63.h, @@ -102,7 +102,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { spacing: 3.h, runSpacing: 4.h, children: [ - AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName!), + AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName ?? ""), AppCustomChipWidget(labelText: "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}", isEnglishOnly: true), AppCustomChipWidget(labelText: "${LocaleKeys.unusedCount.tr(context: context)} ${insuranceApprovalResponseModel.unUsedCount}", isEnglishOnly: true), AppCustomChipWidget(labelText: "${LocaleKeys.companyName.tr(context: context)} ${insuranceApprovalResponseModel.companyName}"), diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index 34662ef4..4bc98627 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -107,7 +107,7 @@ class InsuranceApprovalCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!, + url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL, iconColor: AppColors.blackBgColor, width: 63.h, height: 63.h, diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart index 933f399c..ff0efbf4 100644 --- a/lib/presentation/lab/alphabeticScroll.dart +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -133,7 +133,6 @@ class _AlphabetScrollPageState extends State { @override Widget build(BuildContext context) { - return SizedBox( width: MediaQuery.sizeOf(context).width, child: Row( @@ -149,7 +148,6 @@ class _AlphabetScrollPageState extends State { ], ), ), - SizedBox( width: 24.w, height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 29aa9dc7..0e8890ba 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:ui' as ui; 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'; 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/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/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 CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), 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, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // 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: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[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: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.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: (group.doctorName ?? group.doctorNameEnglish ?? "") .toString() .toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate( DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), 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( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}" .toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider( color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, 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 +import 'dart:async'; import 'dart:ui' as ui; 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'; 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/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/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 CollapsingListView( title: LocaleKeys.labResults.tr(context: context), physics: activeIndex == 1 ? NeverScrollableScrollPhysics() : ScrollPhysics(), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), 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, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), ], 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: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[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: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.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: (group.doctorName ?? group.doctorNameEnglish ?? "") .toString() .toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate( DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), 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( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}" .toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), (_appState.isArabic() ? order.testDetails![index].testDescriptionAr! : order.testDetails![index].testDescriptionEn!).toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider( color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, 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 d03c02df..fcb8251f 100644 --- a/lib/presentation/lab/lab_result_item_view.dart +++ b/lib/presentation/lab/lab_result_item_view.dart @@ -56,7 +56,7 @@ class LabResultItemView extends StatelessWidget { Row( children: [ Utils.buildImgWithNetwork( - url: isLoading ? "" : labOrder!.doctorImageURL!, + url: isLoading ? "" : labOrder?.doctorImageURL??"", iconColor: AppColors.transparent, width: 24.h, height: 24.h, diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 8aedd8a5..88b9cb6a 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -1,45 +1,275 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.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/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/lab_result.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart'; -import 'package:provider/provider.dart' show Selector, Provider, ReadContext; +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:provider/provider.dart'; +import '../../../core/dependencies.dart' show getIt; +import '../../../core/utils/size_utils.dart' show ResponsiveExtension; +import '../../../generated/locale_keys.g.dart' show LocaleKeys; -import '../../../widgets/date_range_selector/viewmodel/date_range_view_model.dart'; +class LabResultList extends StatefulWidget { + @override + State createState() => _LabResultListState(); +} -class LabResultList extends StatelessWidget { +class _LabResultListState extends State { late LabViewModel model; + int expandedIndex = -1; + Map descriptionExpandedMap = {}; @override Widget build(BuildContext context) { model = Provider.of(context); - return Selector>( - selector: (_, model) => model.mainLabResultsByHospitals, - builder: (__, list, ___) { - if (list.isEmpty && context.read().labSpecialResult.isEmpty) { + return Selector>>( + selector: (_, model) => model.mainLabResultsByPackageName, + builder: (__, groupedResults, ___) { + if (groupedResults.isEmpty && context.read().labSpecialResult.isEmpty) { // return Utils.getNoDataWidget(context, // noDataText: LocaleKeys.noLabResults.tr(context: context)); return Container(); } else { + // Convert map entries to a list for indexed access + final packageEntries = groupedResults.entries.toList(); + return ListView.builder( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, - shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { - var labItem = list[index]; - return LabOrderResultItem(onTap: () { - context.read().flush(); - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? ""); - }, - tests: labItem, - index: index, - iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), - severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")); - }); + shrinkWrap: true, + itemCount: packageEntries.length, + itemBuilder: (context, packageIndex) { + final packageName = packageEntries[packageIndex].key; + final labResults = packageEntries[packageIndex].value; + final isExpanded = expandedIndex == packageIndex; + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Package name as title/header + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: packageName.toText18( + isBold: true + ), + ), + AnimatedRotation( + turns: isExpanded ? 0.5 : 0, + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Icon(Icons.expand_more), + ), + ], + ), + if(labResults.first.packageShortDescription != null) + _ExpandableDescriptionText( + description: labResults.first.packageShortDescription!, + isExpanded: descriptionExpandedMap[packageIndex] ?? false, + onToggle: () { + setState(() { + descriptionExpandedMap[packageIndex] = !(descriptionExpandedMap[packageIndex] ?? false); + }); + }, + ) + else + SizedBox.shrink() + ], + ),), + + // Lab results under this package with animation + AnimatedSize( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: isExpanded + ? Column( + children: labResults.asMap().entries.map((entry) { + final index = entry.key; + final labItem = entry.value; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 50)), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: child, + ), + ); + }, + child: Column( + children: [ + LabOrderResultItem( + onTap: () { + context.read().flush(); + model.getPatientLabResult( + model.currentlySelectedPatientOrder!, + labItem.description ?? "", + labItem.testShortDescription ?? "", + labItem.uOM ?? "" + ); + }, + tests: labItem, + index: index, + iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), + severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N") + ), + if(labResults.length>1) + ...[SizedBox( + height: 8.h, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h), + child: Divider( + color: AppColors.dividerColor, + thickness: 1.h, + ), + )] + ], + ), + ); + }).toList(), + ) + : SizedBox.shrink(), + ), + ], + ), + ).onPress((){ + setState(() { + expandedIndex = isExpanded ? -1 : packageIndex; + }); + }); + } + ); + } + }, + ); + } +} + +class _ExpandableDescriptionText extends StatelessWidget { + final String description; + final bool isExpanded; + final VoidCallback onToggle; + + const _ExpandableDescriptionText({ + required this.description, + required this.isExpanded, + required this.onToggle, + }); + + static TextStyle get textStyle => TextStyle( + fontSize: 14.f, + color: AppColors.textColorLight, + // or Colors.black87 + // fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + fontStyle: FontStyle.normal, + height: 1.4, + letterSpacing: 0.0, + decoration: TextDecoration.none, + ); + + @override + Widget build(BuildContext context) { + final readMoreText = isExpanded + ? ' ${LocaleKeys.hide.tr()}' + : ' ${LocaleKeys.readMore.tr(context: context)}'; + + final readMoreStyle = TextStyle( + fontSize: 12.f, + color: AppColors.primaryRedColor, + // fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ); + + return LayoutBuilder( + builder: (context, constraints) { + // Check if description alone overflows 2 lines + final descriptionPainter = TextPainter( + text: TextSpan(text: description, style: textStyle), + maxLines: 2, + textDirection: Directionality.of(context), + )..layout(maxWidth: constraints.maxWidth); + + final isOverflowing = descriptionPainter.didExceedMaxLines; + + // No overflow, or already expanded -> show full text + if (!isOverflowing || isExpanded) { + return GestureDetector( + onTap: isOverflowing ? onToggle : null, + child: Text.rich( + TextSpan( + style: textStyle, + children: [ + TextSpan(text: description, style: textStyle), + if (isOverflowing) + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + ), + ); + } + + // Binary search for max chars that fit in 2 lines with "Read more" appended + int low = 0, high = description.length; + while (low < high) { + final mid = (low + high + 1) ~/ 2; + final testPainter = TextPainter( + text: TextSpan( + style: textStyle, + children: [ + TextSpan(text: '${description.substring(0, mid)}...'), + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + maxLines: 2, + textDirection: Directionality.of(context), + )..layout(maxWidth: constraints.maxWidth); + + if (testPainter.didExceedMaxLines) { + high = mid - 1; + } else { + low = mid; + } } + + final truncated = description.substring(0, low).trimRight(); + + return GestureDetector( + onTap: onToggle, + child: Text.rich( + TextSpan( + style: textStyle, + children: [ + TextSpan(text: '$truncated...'), + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + ), + ); }, ); } -} \ No newline at end of file +} 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 ad2ad77f..2f6ee667 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 @@ -4,6 +4,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/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/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -12,6 +13,7 @@ import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.d 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:provider/provider.dart'; import 'dart:ui' as ui; @@ -24,7 +26,16 @@ class LabOrderResultItem extends StatelessWidget { final bool isExpanded; final Color iconColor; - const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText}); + const LabOrderResultItem({ + super.key, + required this.onTap, + this.tests, + required this.index, + this.isLoading = false, + this.isExpanded = false, + required this.iconColor, + required this.severityText + }); @override build(BuildContext context) { @@ -32,10 +43,10 @@ class LabOrderResultItem extends StatelessWidget { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), child: Container( key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 4.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -84,7 +95,20 @@ class LabOrderResultItem extends StatelessWidget { ), softWrap: true, ), - ) + ), + Visibility( + visible: tests?.notes != null && tests?.notes?.isNotEmpty == true, + child: Text( + tests?.notes??"", + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ), ], ), ), @@ -104,7 +128,9 @@ class LabOrderResultItem extends StatelessWidget { severityText.tr().toText10(isBold: true, color: AppColors.greyTextColor), Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor), ], - ), + ).onPress((){ + _buildSeverityDetailsBottomsheet(severity: tests?.calculatedResultFlag ?? "N", context: context); + }), ), ), Flexible( @@ -136,4 +162,77 @@ class LabOrderResultItem extends StatelessWidget { ), )); } + + _buildSeverityDetailsBottomsheet({required String severity,required BuildContext context}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.colorScience.tr(context: context), + context, + child: Container( + padding: EdgeInsets.all(16.w), + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildInfoTile(severityValue: severity, context: context), + ], + ), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildInfoTile({required String severityValue, required BuildContext context}) { + + String title = LocaleKeys.emergency.tr(context: context); + String description = LocaleKeys.emergencyDescription.tr(context: context); + Color dotColor = AppColors.chipColorEmergency; + switch (severityValue) { + case 'CL': + case 'LCL': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + case 'L': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'N': + title = LocaleKeys.monitor.tr(context: context); + description = LocaleKeys.monitorDescription.tr(context: context); + dotColor = AppColors.chipColorMonitor; + case 'H': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'CH': + case 'HCH': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 14.h, + width: 14.w, + decoration: BoxDecoration(shape: BoxShape.circle, color: dotColor), + ).paddingOnly(top: 3.h), + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(isBold: true, color: AppColors.textColor), + SizedBox(height: 4.h), + description.toText12(isBold: true, color: AppColors.greyTextColor), + ], + ), + ), + ], + ); + } } diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 66135dda..fbc687cc 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -110,16 +110,19 @@ class _MedicalFilePageState extends State { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - myAppointmentsViewModel.getPatientMyDoctors(); - prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); - medicalFileViewModel.setIsPatientSickLeaveListLoading(true); - medicalFileViewModel.getPatientSickLeaveList(); - medicalFileViewModel.onTabChanged(0); - medicalFileViewModel.setIsPatientMedicalReportsLoading(true); - medicalFileViewModel.getPatientMedicalReportList(); // Load vital signs hmgServicesViewModel.getPatientVitalSign(); + + // myAppointmentsViewModel.getPatientMyDoctors(isTop8: true); + // prescriptionsViewModel.initPrescriptionsViewModel(); + + // medicalFileViewModel.setIsPatientSickLeaveListLoading(true); + // medicalFileViewModel.getPatientSickLeaveList(); + // medicalFileViewModel.setIsPatientMedicalReportsLoading(true); + // medicalFileViewModel.getPatientMedicalReportList(); + + medicalFileViewModel.onTabChanged(0); } }); super.initState(); @@ -236,8 +239,8 @@ class _MedicalFilePageState extends State { children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.6, - child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" - .toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2), + child: + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2), ), SizedBox(height: 4.h), Wrap( @@ -481,6 +484,20 @@ class _MedicalFilePageState extends State { expansionMode: ExpansionMode.exactlyOne, dividerColor: AppColors.dividerColor, itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), + onItemToggled: (index, isExpanded) { + debugPrint("Item at index $index is now ${isExpanded ? 'expanded' : 'collapsed'}"); + + if (index == 0 && isExpanded) { + myAppointmentsViewModel.getPatientMyDoctors(isTop8: true); + prescriptionsViewModel.initPrescriptionsViewModel(); + } + if (index == 1 && isExpanded) { + medicalFileViewModel.setIsPatientSickLeaveListLoading(true); + medicalFileViewModel.getPatientSickLeaveList(); + medicalFileViewModel.setIsPatientMedicalReportsLoading(true); + medicalFileViewModel.getPatientMedicalReportList(); + } + }, items: [ ExpandableListItem( title: LocaleKeys.medicalServices.tr(context: context).toText18(isBold: true), @@ -909,7 +926,7 @@ class _MedicalFilePageState extends State { child: Row( children: [ Utils.buildImgWithNetwork( - url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, + url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL, iconColor: AppColors.transparent, width: 40.h, height: 40.h, @@ -1046,6 +1063,7 @@ class _MedicalFilePageState extends State { ], ).onPress(() { // myAppointmentsViewModel.getPatientMyDoctors(); + myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), @@ -1099,7 +1117,7 @@ class _MedicalFilePageState extends State { itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( position: index, - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 250), child: SlideAnimation( horizontalOffset: 100.0, child: FadeInAnimation( @@ -1109,7 +1127,7 @@ class _MedicalFilePageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL, iconColor: AppColors.transparent, width: 64.h, height: 64.h, diff --git a/lib/presentation/medical_file/patient_sickleaves_list_page.dart b/lib/presentation/medical_file/patient_sickleaves_list_page.dart index 431a8faf..f25cd1cf 100644 --- a/lib/presentation/medical_file/patient_sickleaves_list_page.dart +++ b/lib/presentation/medical_file/patient_sickleaves_list_page.dart @@ -142,7 +142,7 @@ class _PatientSickleavesListPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Utils.buildImgWithNetwork( - url: model.patientSickLeaveList[index].doctorImageURL!, + url: model.patientSickLeaveList[index].doctorImageURL, iconColor: AppColors.transparent, width: 24.h, height: 24.h, diff --git a/lib/presentation/medical_file/widgets/medical_report_card.dart b/lib/presentation/medical_file/widgets/medical_report_card.dart index 1dc080c7..1a43b83b 100644 --- a/lib/presentation/medical_file/widgets/medical_report_card.dart +++ b/lib/presentation/medical_file/widgets/medical_report_card.dart @@ -92,7 +92,7 @@ class MedicalReportCard extends StatelessWidget { // fontWeight: FontWeight.w600, isBold: true, borderRadius: 12.r, - height: 48.h, + height: 40.h, ).toShimmer2(isShow: isLoading), ], ), 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 63aac378..f06e6a0e 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -57,7 +57,7 @@ class PatientSickLeaveCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!, + url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL, iconColor: AppColors.transparent, width: 30.h, height: 30.h, diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index e03a53a0..9463debe 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -242,7 +242,7 @@ class _MedicalReportsPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Utils.buildImgWithNetwork( - url: report.doctorImageURL!, + url: report.doctorImageURL ?? "", iconColor: AppColors.transparent, width: 24.h, height: 24.h, 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 f074022e..6f601481 100644 --- a/lib/presentation/medical_report/widgets/patient_medical_report_card.dart +++ b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart @@ -43,7 +43,7 @@ class PatientMedicalReportCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL!, + url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL, iconColor: AppColors.transparent, width: 63.h, height: 63.h, diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 4eebc75c..4b6f820a 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -16,6 +16,7 @@ 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/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; class FamilyMedicalScreen extends StatefulWidget { @@ -32,9 +33,15 @@ class _FamilyMedicalScreenState extends State { void initState() { super.initState(); medicalVM = context.read(); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if(medicalVM!.isFamilyFilesLoading) { + LoaderBottomSheet.showLoader(); + } else { + LoaderBottomSheet.hideLoader(); + } medicalVM?.onFamilyFileTabChange(0); medicalVM?.getAllPendingRecordsByResponseId(); + }); } @@ -90,14 +97,22 @@ class _FamilyMedicalScreenState extends State { ), appState.isChildLoggedIn ? SizedBox() : SizedBox(height: 25.h), Selector patientFiles, List pendingFiles})>( - selector: (_, model) => (selectedIndex: model.getSelectedFamilyFileTabIndex, patientFiles: model.patientFamilyFiles, pendingFiles: model.pendingFamilyFiles), + selector: (_, model) => ( + selectedIndex: model.getSelectedFamilyFileTabIndex, + patientFiles: model.patientFamilyFiles, + pendingFiles: model.pendingFamilyFiles, + ), shouldRebuild: (previous, next) { // Only rebuild if something actually changed return previous.selectedIndex != next.selectedIndex || !identical(previous.patientFiles, next.patientFiles) || !identical(previous.pendingFiles, next.pendingFiles); }, - builder: (context, data, child) => getFamilyTabs(index: data.selectedIndex, patientFiles: data.patientFiles, pendingFiles: data.pendingFiles), + builder: (context, data, child) => getFamilyTabs( + index: data.selectedIndex, + patientFiles: data.patientFiles, + pendingFiles: data.pendingFiles, + ), ), SizedBox(height: 20.h), ], @@ -112,6 +127,7 @@ class _FamilyMedicalScreenState extends State { }) { switch (index) { case 0: + return FamilyCards( profiles: patientFiles, onSelect: (FamilyFileResponseModelLists profile) { diff --git a/lib/presentation/notifications/notification_details_page.dart b/lib/presentation/notifications/notification_details_page.dart index abe8b521..70f87739 100644 --- a/lib/presentation/notifications/notification_details_page.dart +++ b/lib/presentation/notifications/notification_details_page.dart @@ -1,10 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenshot_blocker/flutter_screenshot_blocker.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/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/notifications/models/resp_models/notification_response_model.dart'; +import 'package:hmg_patient_app_new/features/notifications/notifications_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'; @@ -20,28 +23,39 @@ class NotificationDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return CollapsingListView( - title: LocaleKeys.notificationDetails.tr(), - // trailing: IconButton( - // icon: Icon( - // Icons.share_outlined, - // size: 24.h, - // color: AppColors.textColor, - // ), - // onPressed: () { - // _shareNotification(); - // }, - // ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - // Notification content card - _buildNotificationCard(context), - SizedBox(height: 24.h), - ], - ).paddingSymmetrical(24.w, 0.h), + getIt.get().markAsRead(notificationID: notification.id ?? 0); + return ScreenshotBlockerWidget( + blockScreenshots: true, // makes this widget appear black in screenshots + detectScreenshots: true, // listen for screenshot attempts + onScreenshotDetected: (event) { + // event.type → 'screenshot_taken' or 'screenshot_blocked' + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Screenshots are not allowed!')), + // ); + }, + child: CollapsingListView( + title: LocaleKeys.notificationDetails.tr(), + // trailing: IconButton( + // icon: Icon( + // Icons.share_outlined, + // size: 24.h, + // color: AppColors.textColor, + // ), + // onPressed: () { + // _shareNotification(); + // }, + // ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + // Notification content card + _buildNotificationCard(context), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0.h), + ), ), ); } @@ -109,9 +123,9 @@ class NotificationDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.messageNotification.tr().toText14( - weight: FontWeight.w600, - color: AppColors.greyTextColor, - ), + weight: FontWeight.w600, + color: AppColors.greyTextColor, + ), SizedBox(height: 8.h), notification.message!.toText16( weight: FontWeight.w400, @@ -123,8 +137,7 @@ class NotificationDetailsPage extends StatelessWidget { ), // Notification image (if MessageType is "image") - if (notification.messageType != null && - notification.messageType!.toLowerCase() == "image") + if (notification.messageType != null && notification.messageType!.toLowerCase() == "image") Builder( builder: (context) { // Try to get image URL from videoURL or messageTypeData @@ -147,9 +160,9 @@ class NotificationDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.attachedImage.tr().toText14( - weight: FontWeight.w600, - color: AppColors.greyTextColor, - ), + weight: FontWeight.w600, + color: AppColors.greyTextColor, + ), SizedBox(height: 8.h), ClipRRect( borderRadius: BorderRadius.circular(12.h), @@ -177,8 +190,8 @@ class NotificationDetailsPage extends StatelessWidget { ), SizedBox(height: 8.h), LocaleKeys.failedToLoadImage.tr().toText12( - color: AppColors.greyTextColor, - ), + color: AppColors.greyTextColor, + ), SizedBox(height: 4.h), Text( imageUrl!, @@ -205,10 +218,7 @@ class NotificationDetailsPage extends StatelessWidget { ), child: Center( child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, + value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ), ); @@ -249,7 +259,8 @@ ${notification.message ?? 'Notification'} Time: ${_formatTime(notification.isSentOn)} Date: ${_formatDate(notification.isSentOn)} ${notification.notificationType != null ? '\nType: ${notification.notificationType}' : ''} - '''.trim(); + ''' + .trim(); await Share.share(shareText); } @@ -274,4 +285,3 @@ ${notification.notificationType != null ? '\nType: ${notification.notificationTy } } } - diff --git a/lib/presentation/notifications/notifications_list_page.dart b/lib/presentation/notifications/notifications_list_page.dart index e9a9447e..77f9a20a 100644 --- a/lib/presentation/notifications/notifications_list_page.dart +++ b/lib/presentation/notifications/notifications_list_page.dart @@ -49,186 +49,176 @@ class _NotificationsListPageState extends State { return CollapsingListView( title: LocaleKeys.notification.tr(context: context), child: Consumer(builder: (context, notificationsVM, child) { - return Column( - children: [ - Container( - margin: EdgeInsets.symmetric(vertical: 24.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: ListView.builder( - itemCount: notificationsVM.isNotificationsLoading - ? 4 - : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0), - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsetsGeometry.zero, - itemBuilder: (context, index) { - if (notificationsVM.isNotificationsLoading) { - return LabResultItemView( - onTap: () {}, - labOrder: null, - index: index, - isLoading: true, - ); - } - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: GestureDetector( - onTap: () { - notificationsVM.markAsRead(notificationID: notificationsVM.notificationsList[index].id ?? 0); + return Column( + children: [ + Container( + margin: EdgeInsets.symmetric(vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.builder( + itemCount: notificationsVM.isNotificationsLoading + ? 4 + // : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0), + : notificationsVM.notificationsList.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + if (notificationsVM.isNotificationsLoading) { + return LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ); + } + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: GestureDetector( + onTap: () { + notificationsVM.markAsRead(notificationID: notificationsVM.notificationsList[index].id ?? 0); getIt.get().getPatientDashboard(); Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NotificationDetailsPage( - notification: notificationsVM.notificationsList[index], + context, + MaterialPageRoute( + builder: (context) => NotificationDetailsPage( + notification: notificationsVM.notificationsList[index], + ), + ), + ); + }, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Message row with red dot for unread + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: notificationsVM.notificationsList[index].message!.toText16( + isBold: (notificationsVM.notificationsList[index].isRead == false), + weight: (notificationsVM.notificationsList[index].isRead == false) ? FontWeight.w600 : FontWeight.w400, + ), + ), + SizedBox(width: 8.w), + // Red dot for unread notifications ONLY + if (notificationsVM.notificationsList[index].isRead == false) + Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, ), ), - ); - }, - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - // Message row with red dot for unread - Row( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + SizedBox(height: 12.h), + // First row: Time and Date chips with arrow + Row( + children: [ + // Time chip with clock icon + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Directionality( + textDirection: ui.TextDirection.ltr, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: notificationsVM.notificationsList[index].message!.toText16( - isBold: (notificationsVM.notificationsList[index].isRead == false), - weight: (notificationsVM.notificationsList[index].isRead == false) - ? FontWeight.w600 - : FontWeight.w400, - ), - ), - SizedBox(width: 8.w), - // Red dot for unread notifications ONLY - if (notificationsVM.notificationsList[index].isRead == false) - Container( - width: 8.w, - height: 8.w, - decoration: BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - ), + Icon(Icons.access_time, size: 12.w, color: AppColors.textColor), + SizedBox(width: 4.w), + _formatTime(notificationsVM.notificationsList[index].isSentOn).toText10(weight: FontWeight.w600, color: AppColors.textColor, isEnglishOnly: true), ], ), - SizedBox(height: 12.h), - // First row: Time and Date chips with arrow - Row( + ), + ), + SizedBox(width: 8.w), + // Date chip with calendar icon + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Directionality( + textDirection: ui.TextDirection.ltr, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - // Time chip with clock icon - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Directionality( - textDirection: ui.TextDirection.ltr, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.access_time, size: 12.w, color: AppColors.textColor), - SizedBox(width: 4.w), - _formatTime(notificationsVM.notificationsList[index].isSentOn).toText10( - weight: FontWeight.w600, - color: AppColors.textColor, - isEnglishOnly: true - ), - ], - ), - ), - ), - SizedBox(width: 8.w), - // Date chip with calendar icon - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Directionality( - textDirection: ui.TextDirection.ltr, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor), - SizedBox(width: 4.w), - _formatDate(notificationsVM.notificationsList[index].isSentOn).toText10( - weight: FontWeight.w600, - color: AppColors.textColor, - isEnglishOnly: true - ), - ], - ), - ), - ), - Spacer(), - // Arrow icon - Transform.flip( - flipX: getIt.get().isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 16.w, - height: 16.h, - iconColor: AppColors.greyTextColor, - ), - ), + Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor), + SizedBox(width: 4.w), + _formatDate(notificationsVM.notificationsList[index].isSentOn).toText10(weight: FontWeight.w600, color: AppColors.textColor, isEnglishOnly: true), ], ), - // Second row: Contains Image chip (if MessageType is "image") - if (notificationsVM.notificationsList[index].messageType != null && - notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image") - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor), - SizedBox(width: 4.w), - LocaleKeys.containsImage.toText10( - weight: FontWeight.w600, - color: AppColors.textColor, - ), - ], - ), - ), - ], - ), + ), + ), + Spacer(), + // Arrow icon + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 16.w, + height: 16.h, + iconColor: AppColors.greyTextColor, + ), + ), + ], + ), + // Second row: Contains Image chip (if MessageType is "image") + if (notificationsVM.notificationsList[index].messageType != null && notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image") + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor), + SizedBox(width: 4.w), + LocaleKeys.containsImage.toText10( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ], ), - SizedBox(height: 16.h), - 1.divider, + ), ], ), ), - ), - ), + SizedBox(height: 16.h), + 1.divider, + ], ), - ); - }).paddingSymmetrical(16.w, 0.h), - ).paddingSymmetrical(24.w, 0.h), - SizedBox(height: 16.h), + ), + ), + ), + ), + ); + }).paddingSymmetrical(16.w, 0.h), + ).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), // SizedBox( // key: _bottomKey, // child: "Show more notifications".toText16( @@ -241,9 +231,9 @@ class _NotificationsListPageState extends State { // _scrollToBottom(); // }), SizedBox(height: 24.h), - ], - ); - }), + ], + ); + }), ); } diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart index ed78dffb..1f86852d 100644 --- a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart +++ b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart @@ -94,7 +94,7 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(24.h), child: Utils.buildImgWithNetwork( - url: offer.imageUrl!, + url: offer.imageUrl, iconColor: AppColors.blackBgColor, width: double.infinity, height: 250.h, diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart index 2a26ac24..0cdbde96 100644 --- a/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart +++ b/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart @@ -159,7 +159,7 @@ class _OffersAndDiscountsPageState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)), child: SizedBox.expand( child: Utils.buildImgWithNetwork( - url: offersAndDiscountVM.filteredOffers[index].imageUrl!, + url: offersAndDiscountVM.filteredOffers[index].imageUrl, iconColor: AppColors.blackBgColor, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { diff --git a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart index 968c4b5b..847b6e38 100644 --- a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart +++ b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart @@ -62,7 +62,7 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget { Radius.circular(5.r), ), child: Utils.buildImgWithNetwork( - url: prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl!, + url: prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl, iconColor: AppColors.blackBgColor, fit: BoxFit.cover, width: 60.w, diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index 802c41c2..96f7ede9 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -126,7 +126,7 @@ class _PrescriptionDetailPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Utils.buildImgWithNetwork( - url: widget.prescriptionsResponseModel.doctorImageURL!, + url: widget.prescriptionsResponseModel.doctorImageURL, iconColor: AppColors.blackBgColor, width: 24.h, height: 24.h, diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index fc607986..b0e11af0 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -43,7 +43,7 @@ class PrescriptionItemView extends StatelessWidget { spacing: 8.h, children: [ Utils.buildImgWithNetwork( - url: isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl!, + url: isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl, iconColor: AppColors.blackBgColor, width: 60.h, height: 60.h, diff --git a/lib/presentation/prescriptions/prescriptions_list_page.dart b/lib/presentation/prescriptions/prescriptions_list_page.dart index fcb0b291..071cce87 100644 --- a/lib/presentation/prescriptions/prescriptions_list_page.dart +++ b/lib/presentation/prescriptions/prescriptions_list_page.dart @@ -151,7 +151,7 @@ class _PrescriptionsListPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Utils.buildImgWithNetwork( - url: model.patientPrescriptionOrders[index].doctorImageURL!, + url: model.patientPrescriptionOrders[index].doctorImageURL, iconColor: AppColors.blackBgColor, width: 24.h, height: 24.h, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 2e6b60b6..2e9bb3b7 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -42,6 +42,8 @@ import 'package:in_app_review/in_app_review.dart'; import 'dart:ui' as ui; +import 'widgets/patient_qr_code_widget.dart'; + class ProfileSettings extends StatefulWidget { const ProfileSettings({super.key}); @@ -232,6 +234,17 @@ class ProfileSettingsState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), child: Column( children: [ + actionItem(AppAssets.patientQRCodeIcon, LocaleKeys.viewPatientQR.tr(context: context), () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.patientQR.tr(context: context), + context, + child: PatientQrCodeWidget(), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }), + 1.divider, actionItem(AppAssets.language_change, LocaleKeys.language.tr(context: context), () { showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.language.tr(context: context), child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false); }, trailingLabel: Utils.appState.isArabic() ? "العربية" : "English"), diff --git a/lib/presentation/profile_settings/widgets/patient_qr_code_widget.dart b/lib/presentation/profile_settings/widgets/patient_qr_code_widget.dart new file mode 100644 index 00000000..adc1632e --- /dev/null +++ b/lib/presentation/profile_settings/widgets/patient_qr_code_widget.dart @@ -0,0 +1,56 @@ +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/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 '../../../generated/locale_keys.g.dart'; + +class PatientQrCodeWidget extends StatelessWidget { + PatientQrCodeWidget({super.key}); + + late AppState appState; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Column( + children: [ + SizedBox(height: 8.h), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2), + SizedBox(height: 8.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), + ), + 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), + Image.network( + "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${appState.getAuthenticatedUser()!.patientId.toString()}", + fit: BoxFit.fill, + height: 156.h, + width: 156.h, + ), + SizedBox(height: 16.h), + ], + ); + } +} diff --git a/lib/presentation/profile_settings/widgets/preferred_language_widget.dart b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart index d0f85090..aa0d8c09 100644 --- a/lib/presentation/profile_settings/widgets/preferred_language_widget.dart +++ b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart @@ -74,7 +74,7 @@ class _PreferredLanguageWidgetState extends State { callBackFunc: () async { Navigator.of(GetIt.instance().navigatorKey.currentContext!).pop(); profileSettingsViewModel.getProfileSettings(); - }, isFullScreen: false, isAutoDismiss: true); + }, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false); }, onError: (error) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/profile_settings/widgets/update_email_widget.dart b/lib/presentation/profile_settings/widgets/update_email_widget.dart index 55f848a6..73b50849 100644 --- a/lib/presentation/profile_settings/widgets/update_email_widget.dart +++ b/lib/presentation/profile_settings/widgets/update_email_widget.dart @@ -134,7 +134,7 @@ class _UpdateEmailDialogState extends State { callBackFunc: () async { Navigator.of(getIt().navigatorKey.currentContext!).pop(); profileSettingsViewModel!.getProfileSettings(); - }, isFullScreen: false, isAutoDismiss: true); + }, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false); }, onError: (error) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart index f1a34565..d617ac22 100644 --- a/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart +++ b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart @@ -41,7 +41,7 @@ class _UpdateEmergencyContactDialogState extends State createState() => _OrganSelectorPageState(); } -class _OrganSelectorPageState extends State { +class _OrganSelectorPageState extends State with SingleTickerProviderStateMixin { + static const double _expandedBodyZoomFactor = 1.08; + static const double _expandedSheetHeightFactor = 0.3; + static const bool _enableTutorialOverlay = false; + static const bool _enableSwipeToFlip = false; + late final AppState _appState; late final DialogService dialogService; late final CacheService cacheService; + late final AnimationController _introZoomHintController; + late final Animation _introZoomHintAnimation; bool _showTutorial = false; + bool _hasPlayedIntroZoomHint = false; @override void initState() { @@ -41,10 +49,52 @@ class _OrganSelectorPageState extends State { _appState = getIt.get(); dialogService = getIt(); cacheService = getIt(); + + _introZoomHintController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), + )..addListener(() { + if (mounted) setState(() {}); + }); + _introZoomHintAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 1.25), + weight: 45, + ), + TweenSequenceItem( + tween: Tween(begin: 1.25, end: 1.0), + weight: 55, + ), + ]).animate(CurvedAnimation( + parent: _introZoomHintController, + curve: Curves.easeInOut, + )); + _checkAndShowTutorial(); + _playIntroZoomHint(); + } + + @override + void dispose() { + _introZoomHintController.dispose(); + super.dispose(); + } + + Future _playIntroZoomHint() async { + if (_hasPlayedIntroZoomHint) return; + _hasPlayedIntroZoomHint = true; + + await Future.delayed(const Duration(milliseconds: 700)); + if (!mounted) return; + _introZoomHintController.forward(from: 0); } Future _checkAndShowTutorial() async { + if (!_enableTutorialOverlay) { + _showTutorial = false; + return; + } + // final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false; // if (!hasSeenTutorial) { // Show tutorial after a short delay to ensure the screen is fully built @@ -118,7 +168,11 @@ class _OrganSelectorPageState extends State { Expanded( child: Stack( children: [ - _buildBodyViewer(viewModel), + SafeArea( + top: false, + bottom: false, + child: _buildBodyViewer(viewModel), + ), _buildViewToggleButtons(viewModel), // _buildViewZoomButtons(viewModel), _buildBottomSheet(viewModel), @@ -131,7 +185,7 @@ class _OrganSelectorPageState extends State { ), ), // Tutorial overlay - if (_showTutorial) + if (_enableTutorialOverlay && _showTutorial) PinchZoomTutorialOverlay( onComplete: _onTutorialComplete, ), @@ -152,7 +206,13 @@ class _OrganSelectorPageState extends State { height: 24.h, ), padding: EdgeInsetsDirectional.only(start: 12, end: 12), - onPressed: () => Navigator.pop(context), + onPressed: () { + // Clear selected organs and sheet status when going back + final viewModel = context.read(); + viewModel.clearAllSelections(); + viewModel.setBottomSheetExpanded(false); + Navigator.pop(context); + }, highlightColor: Colors.transparent, ), ), @@ -175,45 +235,82 @@ class _OrganSelectorPageState extends State { Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { return GestureDetector( - onHorizontalDragEnd: (details) { - // Swipe left or right to toggle view - if (details.primaryVelocity != null) { - if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) { - viewModel.toggleView(); - } - } - }, - child: 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: Builder( - key: ValueKey(viewModel.currentView), - builder: (context) { - // Detect female gender from viewModel; allow Arabic value as fallback - final bool isFemale = - (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى')); - - final String bodyAsset = viewModel.currentView == BodyView.front - ? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale) - : (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale); - - return InteractiveBodyWidget( - bodyImageAsset: bodyAsset, - organs: viewModel.currentOrgans, - selectedOrganIds: viewModel.selectedOrganIds, - onOrganTap: viewModel.toggleOrganSelection, - isBodyHidden: viewModel.isBodyHidden, - tooltipOrganId: viewModel.tooltipOrganId, - isArabic: _appState.isArabic(), - zoomScale: viewModel.currentZoomScale, - ); + onHorizontalDragEnd: !_enableSwipeToFlip + ? null + : (details) { + // Swipe left or right to toggle view + if (details.primaryVelocity != null) { + if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) { + viewModel.toggleView(); + } + } }, - ), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool isExpanded = viewModel.isBottomSheetExpanded; + final double screenHeight = MediaQuery.of(context).size.height; + final double extraBottomInset = isExpanded ? screenHeight * _expandedSheetHeightFactor : 0; + final bool isUserZoomedIn = viewModel.currentZoomScale > 1.01; + + Widget buildInteractiveBody({required bool useExpandedZoom}) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: Builder( + key: ValueKey(viewModel.currentView), + builder: (context) { + final bool isFemale = (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى')); + final double effectiveZoomScale = viewModel.currentZoomScale * _introZoomHintAnimation.value; + + final String bodyAsset = viewModel.currentView == BodyView.front + ? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale) + : (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale); + + final body = InteractiveBodyWidget( + bodyImageAsset: bodyAsset, + organs: viewModel.currentOrgans, + selectedOrganIds: viewModel.selectedOrganIds, + onOrganTap: viewModel.toggleOrganSelection, + isBodyHidden: viewModel.isBodyHidden, + tooltipOrganId: viewModel.tooltipOrganId, + isArabic: _appState.isArabic(), + zoomScale: effectiveZoomScale, + ); + + return body; + }, + ), + ); + } + + if (!isExpanded) { + return Padding( + padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), + child: buildInteractiveBody(useExpandedZoom: false), + ); + } + + return SingleChildScrollView( + physics: isExpanded && !isUserZoomedIn ? const ClampingScrollPhysics() : const NeverScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight + extraBottomInset, + ), + child: Padding( + padding: EdgeInsets.only( + bottom: extraBottomInset, + top: 24.h, + ), + child: Padding( + padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), + child: buildInteractiveBody(useExpandedZoom: true), + ), + ), + ), + ); + }, ), ); } @@ -408,37 +505,73 @@ class _OrganSelectorPageState extends State { } Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) { + final organCount = viewModel.selectedOrgans.length; + final showBadge = !viewModel.isBottomSheetExpanded && organCount > 0; + return PositionedDirectional( end: 24.w, top: -24.h, child: GestureDetector( onTap: viewModel.toggleBottomSheet, behavior: HitTestBehavior.opaque, - child: Container( + child: SizedBox( 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: Transform.flip( - flipX: _appState.isArabic(), - 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, + child: Stack( + clipBehavior: Clip.none, + children: [ + Align( + alignment: Alignment.center, + child: Container( + width: 48.w, + height: 48.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 11.r, + ), + child: Center( + child: Transform.flip( + flipX: _appState.isArabic(), + 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, + ), + ), + ), ), ), ), - ), + if (showBadge) + PositionedDirectional( + top: 8.h, + end: 8.w, + child: Container( + width: 22.w, + height: 22.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.circle, + border: Border.all( + color: AppColors.whiteColor, + width: 1.5, + ), + ), + child: Text( + '$organCount', + style: TextStyle( + color: AppColors.whiteColor, + fontSize: 10.f, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], ), ), ), diff --git a/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart index a32709f9..813ff7e3 100644 --- a/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart +++ b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -7,7 +8,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.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'; -import 'package:vector_math/vector_math_64.dart' show Vector3; class InteractiveBodyWidget extends StatefulWidget { final String bodyImageAsset; @@ -63,9 +63,12 @@ class _InteractiveBodyWidgetState extends State { @override void dispose() { _transformationController.dispose(); + currentZoom = 0.0; super.dispose(); } + double currentZoom = 0.0; + Future _loadImageAspectRatio() async { final ByteData data = await rootBundle.load(widget.bodyImageAsset); final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); @@ -77,17 +80,16 @@ class _InteractiveBodyWidgetState extends State { _imageAspectRatio = image.width / image.height; }); } + + _transformationController.addListener(() { + currentZoom = _transformationController.value.getMaxScaleOnAxis(); + setState(() {}); + }); } void _updateZoom(double scale) { - // Get current translation - final currentTransform = _transformationController.value; - final currentTranslation = currentTransform.getTranslation(); - - // Create new transformation with updated scale while preserving translation - final newTransform = Matrix4.identity() - ..setTranslation(currentTranslation) - ..scaleByVector3(Vector3(scale, scale, 1.0)); + // Keep programmatic zoom centered to avoid drifting the body off-screen. + final newTransform = Matrix4.identity()..scaleByDouble(scale, scale, 1.0, 1.0); _transformationController.value = newTransform; } @@ -103,8 +105,12 @@ class _InteractiveBodyWidgetState extends State { return Center( child: InteractiveViewer( transformationController: _transformationController, - minScale: 0.5, - maxScale: 4.0, + alignment: Alignment.center, + panEnabled: true, + scaleEnabled: true, + boundaryMargin: currentZoom > 1.2 ? EdgeInsets.all(200.h) : EdgeInsets.zero, + minScale: 1.0, + maxScale: 9.0, clipBehavior: Clip.none, child: AspectRatio( aspectRatio: _imageAspectRatio!, @@ -135,7 +141,7 @@ class _InteractiveBodyWidgetState extends State { // Organ dots ...widget.organs.map((organ) { final isSelected = widget.selectedOrganIds.contains(organ.id); - final dotSize = 16.0; + final dotSize = 18.0; final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2); final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2); diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index ae0a4c02..80b79a88 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -132,12 +132,29 @@ class _AncillaryOrderPaymentPageState extends State { if (!todoVM.isProcessingPayment) { selectedPaymentMethod = "MADA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID( + widget.projectID, + appState.getAuthenticatedUser()!.patientId!, + ); paytabsViewModel.setPaymentConfiguration( "Ancillary Orders Payment", widget.totalAmount, ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - final paymentAmount = widget.totalAmount; + paytabsViewModel.startCardPayment( + paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "false", + appointmentDate: DateUtil.convertDateToString(DateTime.now()), + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: widget.totalAmount.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId!.toString(), + orderDescription: "Ancillary Orders Payment", + serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(), + projectID: widget.projectID.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + final paymentAmount = widget.totalAmount; final fortId = transactionData.transactionReference; final paymentMethod = selectedPaymentMethod; @@ -254,11 +271,27 @@ class _AncillaryOrderPaymentPageState extends State { if (!todoVM.isProcessingPayment) { selectedPaymentMethod = "VISA"; if (appState.isPaytabsEnabled) { + transID = Utils.getAdvancePaymentTransID( + widget.projectID, + appState.getAuthenticatedUser()!.patientId!, + ); paytabsViewModel.setPaymentConfiguration( "Ancillary Orders Payment", widget.totalAmount, ); - paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { + paytabsViewModel.startCardPayment(paymentOption: selectedPaymentMethod, + liveServiceID: "0", + isScheduleLiveCare: "false", + appointmentDate: DateUtil.convertDateToString(DateTime.now()), + appointmentNo: "0", + doctorID: "0", + transactionID: transID, + amount: widget.totalAmount.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId!.toString(), + orderDescription: "Ancillary Orders Payment", + serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(), + projectID: widget.projectID.toString(), + onSuccess: (PaytabsTransactionResponseModel transactionData) async { final paymentAmount = widget.totalAmount; final fortId = transactionData.transactionReference; final paymentMethod = selectedPaymentMethod; diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index ac7acf9e..be33fe5d 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -23,7 +23,8 @@ abstract class DialogService { Future showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); - Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}); + Future showCommonBottomSheetWithoutH( + {String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}); Future showSuccessBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}); @@ -119,21 +120,15 @@ class DialogServiceImp implements DialogService { } @override - Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future showCommonBottomSheetWithoutH( + {String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight( context, title: label ?? "", child: exceptionBottomSheetWidget( - context: context, - message: message, - okLabel: okLabel, - cancelLabel: cancelLabel, - onOkPressed: onOkPressed, - onCancelPressed: onCancelPressed, - isConfirmButton: isConfirmButton - ), + context: context, message: message, okLabel: okLabel, cancelLabel: cancelLabel, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed, isConfirmButton: isConfirmButton), callBackFunc: () {}, ); } @@ -240,7 +235,8 @@ class DialogServiceImp implements DialogService { } } -Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) { +Widget exceptionBottomSheetWidget( + {required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ (message).toText16(isBold: false, color: AppColors.textColor), diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index a83d4e3d..018e46b6 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/routes/app_routes.dart'; class NavigationService { final GlobalKey navigatorKey = GlobalKey(); + final RouteObserver> routeObserver = RouteObserver>(); BuildContext? get context => navigatorKey.currentContext; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index e3224258..f4465f7f 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -16,10 +16,7 @@ class AppTheme { visualDensity: VisualDensity.adaptivePlatformDensity, brightness: Brightness.light, pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder() - }, + builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()}, ), hintColor: Colors.grey[400], disabledColor: Colors.grey[300], @@ -52,10 +49,7 @@ class AppTheme { visualDensity: VisualDensity.adaptivePlatformDensity, brightness: Brightness.dark, pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder() - }, + builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()}, ), hintColor: Colors.grey[600], disabledColor: Colors.grey[700], @@ -64,8 +58,7 @@ class AppTheme { scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, highlightColor: Colors.grey[800]!.withOpacity(0.4), splashColor: Colors.transparent, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: Colors.black.withOpacity(0)), + bottomSheetTheme: BottomSheetThemeData(backgroundColor: Colors.black.withOpacity(0)), floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), appBarTheme: AppBarTheme( color: AppColors.dark.scaffoldBgColor, diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index 9fe70ddd..3731ef40 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -43,7 +43,8 @@ class AttachmentOptions extends StatelessWidget { ).onPress(() async { await onGalleryTap(); }), - if (!showFilesOption) SizedBox(height: 16.h), + // if (!showFilesOption) commented because the ui issue. + SizedBox(height: 16.h), if (showFilesOption) checkInOptionCard( appState, diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index f3eb94fb..82b62507 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -6,7 +6,7 @@ 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 { +class CustomButton extends StatefulWidget { final String text; final String? icon; final VoidCallback? onPressed; @@ -54,25 +54,61 @@ class CustomButton extends StatelessWidget { this.applyThemeColor = true, }); + @override + State createState() => _CustomButtonState(); +} + +class _CustomButtonState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 80), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _playTapAnimation() async { + if (!widget.isDisabled && widget.onPressed != null) { + await _animationController.forward(); + _animationController.reverse(); + } + } + @override Widget build(BuildContext context) { - final radius = borderRadius ?? (12.r); - final iconS = iconSize ?? (24.h); - final fontS = fontSize ?? (16.f); - final isButtonDisabled = isDisabled || onPressed == null; + final radius = widget.borderRadius ?? (12.r); + final iconS = widget.iconSize ?? (24.h); + final fontS = widget.fontSize ?? (16.f); + final isButtonDisabled = widget.isDisabled || widget.onPressed == null; return Semantics( button: true, enabled: !isButtonDisabled, - label: text, - hint: isButtonDisabled ? 'Button is disabled' : 'Tap to $text', - child: GestureDetector( + label: widget.text, + hint: isButtonDisabled ? 'Button is disabled' : 'Tap to ${widget.text}', + child: GestureDetector( onTap: isButtonDisabled - ? null - : () { + ? null + : () async { + // Play tap animation and wait for it to complete + await _playTapAnimation(); + // Log button tap to Clarity before executing callback try { - Clarity.setCustomTag('button_text', text.length > 100 ? text.substring(0, 100) : text); + Clarity.setCustomTag('button_text', widget.text.length > 100 ? widget.text.substring(0, 100) : widget.text); Clarity.setCustomTag('button_action', 'tap'); Clarity.setCustomTag('button_disabled', 'false'); Clarity.sendCustomEvent('button_tapped'); @@ -81,67 +117,73 @@ class CustomButton extends StatelessWidget { } // Execute the actual button callback - onPressed?.call(); + widget.onPressed?.call(); }, - child: Container( - height: height == null - ? 56.h + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) => Transform.scale( + scale: _scaleAnimation.value, + child: child, + ), + child: Container( + height: widget.height == null + ? 56.h : (isFoldable || isTablet) - ? height! + 10.h - : height, - width: width, - padding: padding, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor, - borderRadius: radius, + ? widget.height! + 10.h + : widget.height, + width: widget.width, + padding: widget.padding, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : widget.backgroundColor, + borderRadius: radius, customBorder: BorderRadius.circular(radius), - side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)), - child: Row( + side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : widget.borderColor)), + child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (icon != null) - Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, - child: Utils.buildSvgWithAssets( - icon: icon!, - iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor, - isDisabled: isButtonDisabled, + if (widget.icon != null) + Padding( + padding: widget.text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, + child: Utils.buildSvgWithAssets( + icon: widget.icon!, + iconColor: isButtonDisabled ? AppColors.greyTextColor : widget.iconColor, + isDisabled: isButtonDisabled, width: iconS, height: iconS, - applyThemeColor: applyThemeColor), - ), - if (text.isNotEmpty && icon != null) - Flexible( + applyThemeColor: widget.applyThemeColor), + ), + if (widget.text.isNotEmpty && widget.icon != null) + Flexible( child: Text( - text, - overflow: textOverflow ?? TextOverflow.ellipsis, - maxLines: 1, + widget.text, + overflow: widget.textOverflow ?? TextOverflow.ellipsis, + maxLines: 1, textAlign: TextAlign.center, style: context.dynamicTextStyle( fontSize: fontS, - color: isButtonDisabled ? AppColors.greyTextColor : textColor, - letterSpacing: 0, - fontWeight: isBold ? FontWeight.w700 : fontWeight, - ), + color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor, + letterSpacing: 0, + fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight, + ), ), ), - if (text.isNotEmpty && icon == null) - Text( - text, - overflow: textOverflow ?? TextOverflow.ellipsis, - maxLines: 1, + if (widget.text.isNotEmpty && widget.icon == null) + Text( + widget.text, + overflow: widget.textOverflow ?? TextOverflow.ellipsis, + maxLines: 1, textAlign: TextAlign.center, style: context.dynamicTextStyle( fontSize: fontS, - color: isButtonDisabled ? AppColors.greyTextColor : textColor, - letterSpacing: 0, - fontWeight: isBold ? FontWeight.w700 : fontWeight, - ), + color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor, + letterSpacing: 0, + fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight, + ), ), ], ), )), - ); + )); } } diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index c3e78a6a..8d07a4ce 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -257,8 +257,7 @@ void showCommonBottomSheetWithoutHeight( duration: Duration(milliseconds: 500), reverseDuration: Duration(milliseconds: 300), ), - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width //MediaQuery.of(context).size.width, // Full width - ), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), context: context, isScrollControlled: true, showDragHandle: false, @@ -273,9 +272,9 @@ void showCommonBottomSheetWithoutHeight( if (navContext != null && Navigator.canPop(navContext)) { Navigator.of(navContext).pop(); } - if (callBackFunc != null) { - callBackFunc(); - } + // if (callBackFunc != null) { + // callBackFunc(); + // } }); } return SafeArea( diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart index c1ef39f1..44869e58 100644 --- a/lib/widgets/dropdown/country_dropdown_widget.dart +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -1,3 +1,5 @@ +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'; @@ -9,7 +11,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/generated/locale_keys.g.dart'; -import 'dart:ui' as ui; class CustomCountryDropdown extends StatefulWidget { final List countryList; @@ -35,7 +36,9 @@ class CustomCountryDropdown extends StatefulWidget { class CustomCountryDropdownState extends State { CountryEnum? selectedCountry; - late OverlayEntry _overlayEntry; + OverlayEntry? _overlayEntry; + Timer? _showDropdownTimer; + Timer? _refocusTimer; bool _isDropdownOpen = false; FocusNode textFocusNode = FocusNode(); @@ -55,6 +58,9 @@ class CustomCountryDropdownState extends State { @override void dispose() { + _showDropdownTimer?.cancel(); + _refocusTimer?.cancel(); + _removeOverlayEntry(); textFocusNode.dispose(); super.dispose(); } @@ -74,9 +80,18 @@ class CustomCountryDropdownState extends State { }, child: Row( children: [ - Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h, applyThemeColor: false), + Utils.buildSvgWithAssets( + icon: selectedCountry != null + ? selectedCountry!.iconPath + : AppAssets.ksa, + width: 40.h, + height: 40.h, + applyThemeColor: false), SizedBox(width: 8.h), - Utils.buildSvgWithAssets(icon: _isDropdownOpen ? AppAssets.dropdow_icon : AppAssets.dropdow_icon), + Utils.buildSvgWithAssets( + icon: _isDropdownOpen + ? AppAssets.dropdow_icon + : AppAssets.dropdow_icon), ], ), ), @@ -94,7 +109,11 @@ class CustomCountryDropdownState extends State { children: [ Text( LocaleKeys.phoneNumber.tr(), - style: TextStyle(fontSize: 12.f, height: 1.5, fontWeight: FontWeight.w600, letterSpacing: -1), + style: TextStyle( + fontSize: 12.f, + height: 1.5, + fontWeight: FontWeight.w600, + letterSpacing: -1), ), Row( mainAxisAlignment: MainAxisAlignment.start, @@ -102,9 +121,15 @@ class CustomCountryDropdownState extends State { if (selectedCountry != CountryEnum.others) Text( selectedCountry!.countryCode, - style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.w600, letterSpacing: -0.4, height: 1.5, fontFamily: "Poppins"), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.4, + height: 1.5, + fontFamily: "Poppins"), ), - if (selectedCountry != CountryEnum.others) SizedBox(width: 4.h), + if (selectedCountry != CountryEnum.others) + SizedBox(width: 4.h), if (widget.isEnableTextField) SizedBox( height: 20.h, @@ -113,14 +138,19 @@ class CustomCountryDropdownState extends State { alignment: Alignment.centerLeft, child: TextField( focusNode: textFocusNode, - style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.w600, letterSpacing: -0.4, height: 1.5, fontFamily: "Poppins"), - + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.4, + height: 1.5, + fontFamily: "Poppins"), decoration: InputDecoration( - hintText: selectedCountry == CountryEnum.others ? "001*******" : "", + hintText: selectedCountry == CountryEnum.others + ? "001*******" + : "", isDense: true, border: InputBorder.none, contentPadding: EdgeInsets.zero, - ), keyboardType: TextInputType.phone, onChanged: widget.onPhoneNumberChanged, @@ -136,21 +166,31 @@ class CustomCountryDropdownState extends State { Text( selectedCountry != null ? appState.getLanguageCode() == "ar" - ? selectedCountry!.nameArabic - : selectedCountry!.displayName + ? selectedCountry!.nameArabic + : selectedCountry!.displayName : LocaleKeys.selectCountry.tr(), - style: TextStyle(fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w600, letterSpacing: -0.2), + style: TextStyle( + fontSize: 14.f, + height: 21 / 14, + fontWeight: FontWeight.w600, + letterSpacing: -0.2), ), ], ); } void _openDropdown() { + if (!mounted || _isDropdownOpen || _overlayEntry != null) return; + + _showDropdownTimer?.cancel(); if (textFocusNode.hasFocus) { textFocusNode.unfocus(); // Wait for keyboard to close before calculating position - Future.delayed(Duration(milliseconds: 300), () { - _showDropdown(); + _showDropdownTimer = Timer(const Duration(milliseconds: 300), () { + _showDropdownTimer = null; + if (mounted) { + _showDropdown(); + } }); } else { _showDropdown(); @@ -158,8 +198,18 @@ class CustomCountryDropdownState extends State { } void _showDropdown() { + if (!mounted || _isDropdownOpen || _overlayEntry != null) return; + AppState appState = getIt.get(); - RenderBox renderBox = context.findRenderObject() as RenderBox; + final renderObject = context.findRenderObject(); + final overlay = Overlay.maybeOf(context); + if (renderObject is! RenderBox || + !renderObject.attached || + overlay == null) { + return; + } + + final renderBox = renderObject; Offset offset = renderBox.localToGlobal(Offset.zero); bool isRtl = appState.getLanguageCode() == "ar"; double leftPosition; @@ -169,7 +219,7 @@ class CustomCountryDropdownState extends State { leftPosition = offset.dx; } - _overlayEntry = OverlayEntry( + final overlayEntry = OverlayEntry( builder: (context) => Stack( children: [ Positioned.fill( @@ -185,32 +235,41 @@ class CustomCountryDropdownState extends State { width: !widget.isFromBottomSheet ? renderBox.size.width : 60.h, child: Material( child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 12), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, borderRadius: 12), child: Column( children: widget.countryList .map( (country) => GestureDetector( - onTap: () { - setState(() { - selectedCountry = country; - }); - widget.onCountryChange?.call(country); - _closeDropdown(); - }, - child: Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h, applyThemeColor: false), - if (!widget.isFromBottomSheet) SizedBox(width: 12.h), - if (!widget.isFromBottomSheet) - Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName, - style: TextStyle(fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w600, letterSpacing: -0.2)), - ], - ), - )), - ) + onTap: () => _selectCountry(country), + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.h, horizontal: 8.h), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration(borderRadius: 16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: country.iconPath, + width: 38.h, + height: 38.h, + applyThemeColor: false), + if (!widget.isFromBottomSheet) + SizedBox(width: 12.h), + if (!widget.isFromBottomSheet) + Text( + appState.getLanguageCode() == "ar" + ? country.nameArabic + : country.displayName, + style: TextStyle( + fontSize: 14.f, + height: 21 / 14, + fontWeight: FontWeight.w600, + letterSpacing: -0.2)), + ], + ), + )), + ) .toList(), ), ), @@ -220,7 +279,8 @@ class CustomCountryDropdownState extends State { ), ); - Overlay.of(context)?.insert(_overlayEntry); + overlay.insert(overlayEntry); + _overlayEntry = overlayEntry; setState(() { _isDropdownOpen = true; }); @@ -291,18 +351,61 @@ class CustomCountryDropdownState extends State { // }); // } - void _closeDropdown() { - _overlayEntry.remove(); + void _selectCountry(CountryEnum country) { + if (!mounted) { + _removeOverlayEntry(); + return; + } + + _removeOverlayEntry(); setState(() { + selectedCountry = country; _isDropdownOpen = false; }); + // Notify the parent only after this widget has finished updating and + // removing its overlay. The callback may synchronously close the parent. + widget.onCountryChange?.call(country); + _scheduleTextFieldRefocus(); + } + + void _closeDropdown() { + _showDropdownTimer?.cancel(); + _showDropdownTimer = null; + _removeOverlayEntry(); + + if (!mounted) { + _isDropdownOpen = false; + return; + } + + if (_isDropdownOpen) { + setState(() { + _isDropdownOpen = false; + }); + } + + _scheduleTextFieldRefocus(); + } + + void _removeOverlayEntry() { + final overlayEntry = _overlayEntry; + _overlayEntry = null; + if (overlayEntry == null) return; + + overlayEntry.remove(); + overlayEntry.dispose(); + } + + void _scheduleTextFieldRefocus() { + _refocusTimer?.cancel(); if (widget.isEnableTextField && widget.isFromBottomSheet) { - Future.delayed(Duration(milliseconds: 100), () { + _refocusTimer = Timer(const Duration(milliseconds: 100), () { + _refocusTimer = null; if (mounted && textFocusNode.canRequestFocus) { FocusScope.of(context).requestFocus(textFocusNode); } }); } } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 78176753..81dfad3e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" chewie: dependency: transitive description: @@ -145,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloudflare_turnstile: + dependency: "direct main" + description: + name: cloudflare_turnstile + sha256: b3f2b7606a9d6eabfa38112ee4ae4272c4c75f16ed8e52d99fd01822e3ea0478 + url: "https://pub.dev" + source: hosted + version: "3.8.1" collection: dependency: transitive description: @@ -708,10 +716,10 @@ packages: dependency: "direct main" description: name: flutter_paytabs_bridge - sha256: fc48e68848147a8edf41b27f9068b988c4fb28ff9778bf20560a90be0b4dddfb + sha256: d65396883c661ea945a526c21b2fe859299f25fcd951fe3a733ff224e6ec984c url: "https://pub.dev" source: hosted - version: "2.7.13" + version: "2.7.14" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -728,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.1" + flutter_screenshot_blocker: + dependency: "direct main" + description: + name: flutter_screenshot_blocker + sha256: "20e36b4aaca20259008dbb1403b77af6207d48cd8382a772b9c27619bc116849" + url: "https://pub.dev" + source: hosted + version: "1.0.4" flutter_staggered_animations: dependency: "direct main" description: @@ -1349,26 +1365,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1946,10 +1962,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.11" time: dependency: transitive description: @@ -2231,5 +2247,5 @@ packages: source: hosted version: "1.2.0" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index eafbd513..9a05eb01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,8 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -#version: 0.0.38+37 -version: 0.0.7+1 +version: 0.0.44+45 +#version: 0.0.14+1 environment: sdk: ">=3.6.0 <4.0.0" @@ -104,10 +104,12 @@ dependencies: scrollable_positioned_list: ^0.3.8 in_app_review: ^2.0.11 - flutter_paytabs_bridge: ^2.7.13 + flutter_paytabs_bridge: ^2.7.14 clarity_flutter: 1.9.0 flutter_amazonpaymentservices: ^0.0.13 screen_brightness: ^1.0.1 + flutter_screenshot_blocker: ^1.0.4 + cloudflare_turnstile: ^3.7.2 dev_dependencies: flutter_test: