From c58e3956d6a3745917062dadd343302c398f2242 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Thu, 16 Jul 2026 23:54:07 +0300 Subject: [PATCH 01/73] Push notification changes & updates --- lib/core/utils/push_notification_handler.dart | 25 ++ .../notification_details_page.dart | 7 +- .../notifications_list_page.dart | 327 +++++++++--------- 3 files changed, 188 insertions(+), 171 deletions(-) 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/presentation/notifications/notification_details_page.dart b/lib/presentation/notifications/notification_details_page.dart index ba78a298..70f87739 100644 --- a/lib/presentation/notifications/notification_details_page.dart +++ b/lib/presentation/notifications/notification_details_page.dart @@ -1,11 +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'; @@ -21,9 +23,10 @@ class NotificationDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { + getIt.get().markAsRead(notificationID: notification.id ?? 0); return ScreenshotBlockerWidget( - blockScreenshots: true, // makes this widget appear black in screenshots - detectScreenshots: true, // listen for screenshot attempts + 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( diff --git a/lib/presentation/notifications/notifications_list_page.dart b/lib/presentation/notifications/notifications_list_page.dart index afea0fba..77f9a20a 100644 --- a/lib/presentation/notifications/notifications_list_page.dart +++ b/lib/presentation/notifications/notifications_list_page.dart @@ -49,187 +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), - : 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); + 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( @@ -242,9 +231,9 @@ class _NotificationsListPageState extends State { // _scrollToBottom(); // }), SizedBox(height: 24.h), - ], - ); - }), + ], + ); + }), ); } From 09c31299257104bb2e6f977d1271916fa89a67fa Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 19 Jul 2026 12:11:29 +0300 Subject: [PATCH 02/73] feedback file upload file name field added. --- .../contact_us/contact_us_view_model.dart | 34 +++++++++++++---- .../contact_us/feedback_page.dart | 37 +++++++++++-------- lib/widgets/attachment_options.dart | 3 +- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart index 323eb782..40cab14c 100644 --- a/lib/features/contact_us/contact_us_view_model.dart +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -15,6 +15,19 @@ 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; @@ -35,7 +48,7 @@ class ContactUsViewModel extends ChangeNotifier { String? chatRequestID; - List feedbackAttachmentList = []; + List feedbackAttachmentList = []; List cocItemsList = []; bool isCOCItemsListLoading = false; @@ -168,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(); } @@ -282,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(); @@ -291,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; diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 32359fef..251c720d 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -354,11 +354,10 @@ class FeedbackPage extends StatelessWidget { ImageOptions.showImageOptionsNew( context, true, - (String image, file) { - print(image); - print(file); - // Navigator.pop(context); - contactUsViewModel.addFeedbackAttachment(image); + (String base64Image, file) { + // base64Image: The base64-encoded image string + // file: The File object + contactUsViewModel.addFeedbackAttachment(base64Image, file); }, ); }, @@ -385,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/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index bbc0bc6d..db4164a3 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -48,7 +48,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, From ea0c643ce84e398eb5758cfa74ab3008b6e134de Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 20 Jul 2026 15:14:21 +0300 Subject: [PATCH 03/73] updates --- lib/core/api_consts.dart | 4 ++-- lib/core/services/turnstile_service.dart | 4 ++++ lib/features/authentication/authentication_view_model.dart | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 2c64b13e..8f2deaa3 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.6; + static final double appVersionID = 21.7; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/core/services/turnstile_service.dart b/lib/core/services/turnstile_service.dart index f6665190..b30113e0 100644 --- a/lib/core/services/turnstile_service.dart +++ b/lib/core/services/turnstile_service.dart @@ -18,6 +18,10 @@ class TurnstileService { // 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 { diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 6ec5d2bf..16b84d15 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -998,7 +998,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) { From fe29431246df1ab25cff1fd7b05fb9c893e41319 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 21 Jul 2026 11:07:49 +0300 Subject: [PATCH 04/73] updates --- .../authentication/authentication_view_model.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 16b84d15..fdc16943 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'; @@ -979,12 +980,14 @@ class AuthenticationViewModel extends ChangeNotifier { // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); - // Get Turnstile token if configured - final turnstileToken = await _turnstileService.getTurnstileToken(); - if (turnstileToken != null) { - request.turnstileToken = turnstileToken; - } else { - request.turnstileToken = ""; + if(!kDebugMode) { + // Get Turnstile token if configured + final turnstileToken = await _turnstileService.getTurnstileToken(); + if (turnstileToken != null) { + request.turnstileToken = turnstileToken; + } else { + request.turnstileToken = ""; + } } if (isNeedLoading) { From 3030c13a171608f7f30974edb9d65b54949b1840 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 21 Jul 2026 11:56:31 +0300 Subject: [PATCH 05/73] Updates & fixes --- assets/langs/ar-SA.json | 5 ++--- .../widgets/appointment_doctor_card.dart | 6 +++--- .../habib_wallet/habib_wallet_page.dart | 2 +- .../home/lakum_wallet_details.dart | 18 +++++++++++------- lib/widgets/common_bottom_sheet.dart | 6 +++--- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index ed554e97..d0321ace 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": "تم إرسال البريد الإلكتروني بنجاح.", @@ -1946,7 +1945,7 @@ "transactions": "المعاملات", "pharmacy": "الصيدلية", "visitsOrders": "الزيارات/الطلبات", - "earned": "حصل", + "earned": "مكتسب", "getParkingQR": "الحصول على رمز الاستجابة السريعة لوقوف السيارات", "cannotOpenThisFile": "لا يمكن فتح هذا الملف", "refundDetails": "تفاصيل الاسترداد", diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index f0dd2f5d..6e3f77d1 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -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/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 07df55ea..2814a961 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/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/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index c3e78a6a..c86bc516 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -273,9 +273,9 @@ void showCommonBottomSheetWithoutHeight( if (navContext != null && Navigator.canPop(navContext)) { Navigator.of(navContext).pop(); } - if (callBackFunc != null) { - callBackFunc(); - } + // if (callBackFunc != null) { + // callBackFunc(); + // } }); } return SafeArea( From 006db56f937aa669a2bf171ca3367a86d0173d75 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 23 Jul 2026 11:12:11 +0300 Subject: [PATCH 06/73] no message --- .../medical_file/medical_file_view_model.dart | 17 +++++++-- lib/presentation/my_family/my_family.dart | 38 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index e0abd9d7..9f1a692d 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -431,18 +431,29 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getFamilyFiles({int? status, Function(dynamic)? onSuccess, Function(String)? onError}) async { + 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: () { + if (shouldShowLoader) { + LoaderBottomSheet.hideLoader(); + } onError!(failure.message); }, ), (apiResponse) { if (apiResponse.messageStatus == 2) { // _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 @@ -525,6 +536,9 @@ class MedicalFileViewModel extends ChangeNotifier { pendingFamilyFiles = newPendingFamilyFiles; } + if (shouldShowLoader) { + LoaderBottomSheet.hideLoader(); + } notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -662,13 +676,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/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 4eebc75c..62d1fb62 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,27 @@ class _FamilyMedicalScreenState extends State { void initState() { super.initState(); medicalVM = context.read(); - WidgetsBinding.instance.addPostFrameCallback((_) { - medicalVM?.onFamilyFileTabChange(0); - medicalVM?.getAllPendingRecordsByResponseId(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final vm = medicalVM; + if (vm == null) return; + + final shouldShowInitialLoader = vm.patientFamilyFiles.isEmpty; + if (shouldShowInitialLoader) { + LoaderBottomSheet.showLoader(); + } + + vm.onFamilyFileTabChange(0); + try { + AppState appState = getIt.get(); + if (!appState.getIsChildLoggedIn) { + await vm.getFamilyFiles(status: 0); + } + await vm.getAllPendingRecordsByResponseId(); + } finally { + if (shouldShowInitialLoader) { + LoaderBottomSheet.hideLoader(); + } + } }); } @@ -90,14 +109,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 +139,7 @@ class _FamilyMedicalScreenState extends State { }) { switch (index) { case 0: + return FamilyCards( profiles: patientFiles, onSelect: (FamilyFileResponseModelLists profile) { From 6075da95ea1ee9885c72102ce997276a626d7be4 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 23 Jul 2026 15:33:36 +0300 Subject: [PATCH 07/73] Updates & fixes --- lib/core/api_consts.dart | 2 +- lib/features/insurance/insurance_view_model.dart | 10 ---------- lib/features/my_appointments/my_appointments_repo.dart | 6 +++--- .../my_appointments/my_appointments_view_model.dart | 5 ----- .../insurance/insurance_approval_details_page.dart | 2 +- 5 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8f2deaa3..8f1455ad 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT 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/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 87484e06..d7a926d3 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -172,9 +172,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"); diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index b480a354..52d8b240 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -277,7 +277,6 @@ class MyAppointmentsViewModel extends ChangeNotifier { } if (!isAppointmentDataToBeLoaded) return; - debugPrint('getPatientAppointments CALLED!!!'); patientAppointmentsByClinic.clear(); patientAppointmentsByHospital.clear(); @@ -347,9 +346,6 @@ class MyAppointmentsViewModel extends ChangeNotifier { } // } - debugPrint('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); - debugPrint('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); - debugPrint('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); notifyListeners(); } @@ -1299,7 +1295,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/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index b1fce84a..d0d7f681 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -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}"), From 09df05f11e46810215bd1562cf09bd1e55287247 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 23 Jul 2026 15:57:12 +0300 Subject: [PATCH 08/73] family file loading fix. --- .../medical_file/medical_file_view_model.dart | 37 ++++++++++++++----- lib/presentation/my_family/my_family.dart | 22 +++-------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 9f1a692d..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; @@ -431,6 +435,9 @@ 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(); @@ -439,24 +446,31 @@ class MedicalFileViewModel extends ChangeNotifier { final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.getSuperUserID != null ? _appState.getSuperUserID! : _appState.getAuthenticatedUser()!.patientId!); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - if (shouldShowLoader) { - LoaderBottomSheet.hideLoader(); - } - 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, @@ -536,6 +550,9 @@ class MedicalFileViewModel extends ChangeNotifier { pendingFamilyFiles = newPendingFamilyFiles; } + // Clear loading state on success + setIsFamilyFilesLoading = false; + if (shouldShowLoader) { LoaderBottomSheet.hideLoader(); } diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 62d1fb62..4b6f820a 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -34,26 +34,14 @@ class _FamilyMedicalScreenState extends State { super.initState(); medicalVM = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) async { - final vm = medicalVM; - if (vm == null) return; - - final shouldShowInitialLoader = vm.patientFamilyFiles.isEmpty; - if (shouldShowInitialLoader) { + if(medicalVM!.isFamilyFilesLoading) { LoaderBottomSheet.showLoader(); + } else { + LoaderBottomSheet.hideLoader(); } + medicalVM?.onFamilyFileTabChange(0); + medicalVM?.getAllPendingRecordsByResponseId(); - vm.onFamilyFileTabChange(0); - try { - AppState appState = getIt.get(); - if (!appState.getIsChildLoggedIn) { - await vm.getFamilyFiles(status: 0); - } - await vm.getAllPendingRecordsByResponseId(); - } finally { - if (shouldShowInitialLoader) { - LoaderBottomSheet.hideLoader(); - } - } }); } From 96d049ab05240f5845b70c28532eac4395f29134 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 29 Jul 2026 17:08:47 +0300 Subject: [PATCH 09/73] updates --- assets/images/svg/patient_qr_code_icon.svg | 12 ++++ assets/langs/ar-SA.json | 18 +++--- assets/langs/en-US.json | 18 +++--- lib/core/api_consts.dart | 2 +- lib/core/app_assets.dart | 1 + lib/generated/locale_keys.g.dart | 2 + .../book_appointment_page.dart | 2 +- .../medical_file/medical_file_page.dart | 3 + .../profile_settings/profile_settings.dart | 13 +++++ .../widgets/patient_qr_code_widget.dart | 56 +++++++++++++++++++ 10 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 assets/images/svg/patient_qr_code_icon.svg create mode 100644 lib/presentation/profile_settings/widgets/patient_qr_code_widget.dart 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 d0321ace..b2bc5c2d 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1959,12 +1959,14 @@ "refund": "استرداد", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:", - "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", - "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", - "schedule": "جدول", - "doctorSchedule": "جدول الطبيب", - "weeklyShift": "الفترات الأسبوعية", - "dayOff": "إجازة", - "notAvailable": "غير متاح", - "hide": "إخفاء" + "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", + "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", + "schedule": "جدول", + "doctorSchedule": "جدول الطبيب", + "weeklyShift": "الفترات الأسبوعية", + "dayOff": "إجازة", + "notAvailable": "غير متاح", + "hide": "إخفاء", + "viewPatientQR": "عرض رمز الاستجابة السريعة للمريض", + "patientQR": "رمز الاستجابة السريعة للمريض" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index ae344b01..0e3429e4 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1949,14 +1949,16 @@ "invoiceHistory": "Invoice History", "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", - "hide": "Hide" + "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" } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8f1455ad..8f2deaa3 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index aba22eb3..b4611890 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -399,6 +399,7 @@ 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'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 80cea8b4..51a687a8 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1948,5 +1948,7 @@ abstract class LocaleKeys { static const weeklyShift = 'weeklyShift'; static const dayOff = 'dayOff'; static const hide = 'hide'; + static const viewPatientQR = 'viewPatientQR'; + static const patientQR = 'patientQR'; } diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 55769fbe..9b210eee 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -67,7 +67,7 @@ class _BookAppointmentPageState extends State { immediateLiveCareViewModel.initImmediateLiveCare(); if (appState.isAuthenticated) { getIt.get().getPatientMyDoctors(isTop8: true); - getIt.get().getPatientFavouriteDoctors(); + // getIt.get().getPatientFavouriteDoctors(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index b382bf01..86e02da3 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -481,6 +481,9 @@ class _MedicalFilePageState extends State { expansionMode: ExpansionMode.exactlyOne, dividerColor: AppColors.dividerColor, itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), + onItemToggled: (index, isExpanded) { + print("Item at index $index is now ${isExpanded ? 'expanded' : 'collapsed'}"); + }, items: [ ExpandableListItem( title: LocaleKeys.medicalServices.tr(context: context).toText18(isBold: true), 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), + ], + ); + } +} From 160bee5ffec25255b1de17e3c2fe0b6fcc363fa2 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Thu, 30 Jul 2026 18:58:30 +0300 Subject: [PATCH 10/73] medical file page updates --- .../medical_file/medical_file_page.dart | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 86e02da3..cdee10db 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(isTop8: true); - 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( @@ -482,7 +485,18 @@ class _MedicalFilePageState extends State { dividerColor: AppColors.dividerColor, itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), onItemToggled: (index, isExpanded) { - print("Item at index $index is now ${isExpanded ? 'expanded' : 'collapsed'}"); + 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( From f42fbe19aeb27432e824e30fc12d23df7a9ddcde Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 2 Aug 2026 15:49:01 +0300 Subject: [PATCH 11/73] updates --- android/app/build.gradle.kts | 2 +- android/app/src/main/res/values/colors.xml | 26 ++++++++++++++++++++++ lib/core/app_state.dart | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 android/app/src/main/res/values/colors.xml 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/lib/core/app_state.dart b/lib/core/app_state.dart index a27b727e..e627c7aa 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -50,8 +50,8 @@ class AppState { bool isPayfortHostedPageEnabled = true; void setIsPaytabsEnabled(bool value) { - isPaytabsEnabled = value; - // isPaytabsEnabled = true; + // isPaytabsEnabled = value; + isPaytabsEnabled = true; } void setIsPayfortHostedPageEnabled(bool value) { diff --git a/pubspec.yaml b/pubspec.yaml index bdce8130..006aca7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -104,7 +104,7 @@ 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 From c00fe3e10c0e674339d4aef3d78625c44da98eb6 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 4 Aug 2026 16:29:34 +0300 Subject: [PATCH 12/73] UI/UX updates --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 2 +- lib/core/app_state.dart | 4 +- .../book_appointments_view_model.dart | 16 +- lib/generated/locale_keys.g.dart | 1 + .../region_list_widget.dart | 9 ++ .../book_appointment/select_clinic_page.dart | 4 +- .../book_appointment/select_doctor_page.dart | 3 +- .../widgets/appointment_calendar.dart | 81 ++++++++-- lib/presentation/home/landing_page.dart | 4 +- .../home/widgets/large_service_card.dart | 8 +- .../home/widgets/welcome_widget.dart | 10 +- lib/widgets/buttons/custom_button.dart | 146 +++++++++++------- 15 files changed, 208 insertions(+), 90 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index b2bc5c2d..c025a030 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1968,5 +1968,6 @@ "notAvailable": "غير متاح", "hide": "إخفاء", "viewPatientQR": "عرض رمز الاستجابة السريعة للمريض", - "patientQR": "رمز الاستجابة السريعة للمريض" + "patientQR": "رمز الاستجابة السريعة للمريض", + "forText": "عن" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 0e3429e4..69e0d66d 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1958,7 +1958,8 @@ "notAvailable": "Not Available", "hide": "Hide", "viewPatientQR": "View Patient QR Code", - "patientQR": "Patient QR Code" + "patientQR": "Patient QR Code", + "forText": "For" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 3bfbf6cb..4829b58c 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 2122445; + body['TokenID'] = "@dm!n"; + // body['PatientID'] = 5421804; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8f2deaa3..8f1455ad 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index e627c7aa..a27b727e 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -50,8 +50,8 @@ class AppState { bool isPayfortHostedPageEnabled = true; void setIsPaytabsEnabled(bool value) { - // isPaytabsEnabled = value; - isPaytabsEnabled = true; + isPaytabsEnabled = value; + // isPaytabsEnabled = true; } void setIsPayfortHostedPageEnabled(bool value) { diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index e4f028d9..9ed93c0a 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -420,14 +420,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) { diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 51a687a8..536f3355 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1950,5 +1950,6 @@ abstract class LocaleKeys { static const hide = 'hide'; static const viewPatientQR = 'viewPatientQR'; static const patientQR = 'patientQR'; + static const forText = 'forText'; } 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/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index cf1cd897..e0a810ed 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1202,7 +1202,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..132cd46a 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -57,7 +57,8 @@ class _SelectDoctorPageState extends State { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { - bookAppointmentsViewModel.getDoctorsList(); + bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); + bookAppointmentsViewModel.getDoctorsList(isNearest: true); } } }); diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index ea954e03..c84e909f 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -95,20 +95,23 @@ class _AppointmentCalendarState 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( + 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, + // textAlign: isArabic ? TextAlign.end : TextAlign.start, textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor), ), viewHeaderStyle: ViewHeaderStyle( @@ -153,6 +156,56 @@ class _AppointmentCalendarState extends State { ), ), ), + // 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, + ), + ), + ), + ], + ), + ), + ), + ], + ), ), SizedBox(height: 10.h), Transform.translate( @@ -164,7 +217,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( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 85f1e972..b3ba5e78 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -253,8 +253,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: [ 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/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, + ), ), ], ), )), - ); + )); } } From 95d76a41e479408465664da254c4325d10b04b39 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Wed, 5 Aug 2026 18:43:55 +0300 Subject: [PATCH 13/73] updates --- .../book_appointments_view_model.dart | 1 + .../review_appointment_page.dart | 3 + .../widgets/appointment_calendar.dart | 218 +++++++++--------- 3 files changed, 119 insertions(+), 103 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 9ed93c0a..8ec370a4 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(); } diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index b7fb6469..a7e01e4b 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -424,6 +424,7 @@ class _ReviewAppointmentPageState 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/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index c84e909f..11092c79 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -14,8 +14,10 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/review_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; @@ -110,102 +112,102 @@ class _AppointmentCalendarState extends State { 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, + 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), ), - ), - monthViewSettings: MonthViewSettings( - dayFormat: "EEE", - appointmentDisplayMode: MonthAppointmentDisplayMode.indicator, - showTrailingAndLeadingDates: false, - appointmentDisplayCount: 1, - monthCellStyle: MonthCellStyle( - textStyle: TextStyle(fontSize: 19.f), + ), + 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!); + }, ), ), - onTap: (CalendarTapDetails details) { - _calendarController.selectedDate = details.date; - _onDaySelected(details.date!); - }, ), ), - ), - ), - // 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, + // 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, + // 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, + ), + ), ), - ), + ], ), - ], + ), ), - ), + ], ), - ], - ), ), SizedBox(height: 10.h), Transform.translate( @@ -266,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( @@ -284,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), @@ -384,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) { From 43470b4da5dc72a69d851a8f2003b7cf5097021f Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 6 Aug 2026 10:47:46 +0300 Subject: [PATCH 14/73] updates --- lib/core/api/api_client.dart | 2 +- lib/presentation/authentication/register.dart | 75 +++++++++++++------ lib/presentation/lab/alphabeticScroll.dart | 2 - lib/presentation/lab/lab_orders_page.dart | 2 +- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 4829b58c..3a5ff281 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } body['TokenID'] = "@dm!n"; - // body['PatientID'] = 5421804; + // body['PatientID'] = 1307867; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; 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/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 c3a2adfe..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), (_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 +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 From 2be696b952a97a1bc974d31bddf349837b304da6 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sat, 8 Aug 2026 21:26:58 +0300 Subject: [PATCH 15/73] Updates & fixes --- lib/core/api_consts.dart | 2 +- .../authentication_view_model.dart | 18 +++++++++--------- .../appointments/widgets/appointment_card.dart | 9 +++++++++ .../book_appointment/doctor_profile_page.dart | 9 +++++++++ .../widgets/appointment_calendar.dart | 2 +- pubspec.yaml | 4 ++-- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8f1455ad..7744dee7 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.7; + static final double appVersionID = 21.8; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index fdc16943..7688cfa4 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -980,15 +980,15 @@ 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(!kDebugMode) { + // // Get Turnstile token if configured + // final turnstileToken = await _turnstileService.getTurnstileToken(); + // if (turnstileToken != null) { + // request.turnstileToken = turnstileToken; + // } else { + // request.turnstileToken = ""; + // } + // } if (isNeedLoading) { LoaderBottomSheet.showLoader(); diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index e98eda82..91ebaa14 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'; @@ -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/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 6be110ec..a3b4aa48 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -8,6 +8,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'; @@ -240,6 +241,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/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index 11092c79..bc6e214c 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -268,7 +268,7 @@ class _AppointmentCalendarState extends State { ), ); } else { - if (bookAppointmentsViewModel.selectedClinic.clinicID! == 17 && bookAppointmentsViewModel.selectedChiefComplaintID == 0) { + if (bookAppointmentsViewModel.selectedClinic.clinicID == 17 && bookAppointmentsViewModel.selectedChiefComplaintID == 0) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: bookAppointmentsViewModel.selectedClinic.clinicID, clinicDescription: bookAppointmentsViewModel.selectedClinic.clinicDescription, diff --git a/pubspec.yaml b/pubspec.yaml index 006aca7f..df7ce33e 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.39+38 -#version: 0.0.8+1 +#version: 0.0.40+41 +version: 0.0.11+1 environment: sdk: ">=3.6.0 <4.0.0" From d2718c1e279d865cf22a7a6637cfddc87c0c648e Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 9 Aug 2026 10:30:52 +0300 Subject: [PATCH 16/73] saved login issue fixed. --- lib/main.dart | 1 + .../authentication/saved_login_screen.dart | 51 +++++++++++++++---- lib/services/navigation_service.dart | 1 + 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index aa266871..87746f14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -370,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/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/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; From 2532d958958af392ca2ef18a21786b288feaaf2a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 9 Aug 2026 10:37:32 +0300 Subject: [PATCH 17/73] updates --- .../book_appointment_page.dart | 1 + .../book_appointment/doctor_profile_page.dart | 2 + .../hmg_services/services_page.dart | 94 ++++++++++--------- pubspec.yaml | 4 +- 4 files changed, 54 insertions(+), 47 deletions(-) diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 9b210eee..cb93d9e0 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -552,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 a3b4aa48..9b5d6583 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'; @@ -30,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( 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/pubspec.yaml b/pubspec.yaml index df7ce33e..a70a56a8 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.40+41 -version: 0.0.11+1 +version: 0.0.41+42 +#version: 0.0.11+1 environment: sdk: ">=3.6.0 <4.0.0" From 91b999d31361eeccf2ec6cc455ed7571fcc4639a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 9 Aug 2026 11:14:54 +0300 Subject: [PATCH 18/73] updates --- ios/Runner.xcodeproj/project.pbxproj | 24 ++++++++++++++++-------- pubspec.yaml | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) 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/pubspec.yaml b/pubspec.yaml index a70a56a8..1685f8df 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.41+42 -#version: 0.0.11+1 +#version: 0.0.42+43 +version: 0.0.12+1 environment: sdk: ">=3.6.0 <4.0.0" From 64e60456cc776db5867c86c189375fada422edb2 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 9 Aug 2026 16:42:04 +0300 Subject: [PATCH 19/73] updates --- .../my_appointments/my_appointments_repo.dart | 7 +++--- .../my_appointments_view_model.dart | 16 ++++++++----- .../book_appointment/select_doctor_page.dart | 24 +++++++++++++++++-- .../book_appointment/widgets/doctor_card.dart | 7 +++--- lib/presentation/home/landing_page.dart | 2 +- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index d7a926d3..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}); @@ -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 { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 52d8b240..1689b57a 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -55,6 +55,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isAppointmentDataToBeLoaded = true; bool isMyDoctorsDataToBeLoaded = true; + bool isArrivedAppointmentDataLoaded = false; bool isEyeMeasurementsAppointmentsLoading = false; @@ -76,7 +77,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientUpcomingAppointmentsHistoryList = []; List patientArrivedAppointmentsHistoryList = []; - List patientTimelineAppointmentsList = []; + List patientAllArrivedAppointmentsHistoryList = []; List patientMyDoctorsList = []; @@ -172,7 +173,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); - patientTimelineAppointmentsList.clear(); + patientAllArrivedAppointmentsHistoryList.clear(); patientEyeMeasurementsAppointmentsHistoryList.clear(); isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; @@ -266,7 +267,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(); @@ -277,12 +278,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { } if (!isAppointmentDataToBeLoaded) return; - patientAppointmentsByClinic.clear(); patientAppointmentsByHospital.clear(); patientAppointmentsViewList.clear(); - patientTimelineAppointmentsList.clear(); + patientAllArrivedAppointmentsHistoryList.clear(); filteredAppointmentList.clear(); patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); @@ -291,7 +291,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( @@ -320,6 +320,10 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientArrivedAppointmentsHistoryList = apiResponse.data!; isMyAppointmentsLoading = false; isAppointmentDataToBeLoaded = false; + if (!isForTimeLine) { + patientAllArrivedAppointmentsHistoryList = apiResponse.data!; + isArrivedAppointmentDataLoaded = true; + } notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 132cd46a..8813da07 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -134,8 +134,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/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index fe7cf3e0..180edae0 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -167,9 +167,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/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b3ba5e78..2df8f77c 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -493,8 +493,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(); }), From ed653d8108220f6814631fb9d94b45aac0a7691a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 9 Aug 2026 17:47:50 +0300 Subject: [PATCH 20/73] Payment request & response insert implemented in paytabs. --- lib/core/api_consts.dart | 2 +- lib/core/dependencies.dart | 6 +- lib/features/paytabs/paytabs_view_model.dart | 199 +++++++++++++++--- .../appointment_payment_page.dart | 44 +++- .../immediate_livecare_payment_page.dart | 40 +++- .../waiting_appointment_payment_page.dart | 45 +++- .../er_online_checkin_payment_page.dart | 42 +++- .../wallet_payment_confirm_page.dart | 36 +++- .../ancillary_order_payment_page.dart | 39 +++- 9 files changed, 400 insertions(+), 53 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 7744dee7..9fb7050a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index ecfe1324..e4ca4c25 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -369,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/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/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 46130b12..0d864b64 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -139,12 +139,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 +380,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/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index ef21ccbd..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 ).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/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index f894c387..eea9ec10 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, @@ -295,9 +309,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/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; From 8fad8999d7dfd77a910213d281ccf01de3f5cc15 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 13:45:23 +0300 Subject: [PATCH 21/73] isForTimeLine param implemented in my appointments flow --- .../my_appointments_view_model.dart | 52 +++++++++++++++++++ .../appointments/my_appointments_page.dart | 4 +- .../wallet_payment_confirm_page.dart | 1 + lib/presentation/home/landing_page.dart | 4 +- .../update_emergency_contact_widget.dart | 2 +- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 1689b57a..88fb8e64 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -56,6 +56,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isAppointmentDataToBeLoaded = true; bool isMyDoctorsDataToBeLoaded = true; bool isArrivedAppointmentDataLoaded = false; + bool isFullArrivedAppointmentsLoaded = false; bool isEyeMeasurementsAppointmentsLoading = false; @@ -178,6 +179,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; patientMyDoctorsList.clear(); + isFullArrivedAppointmentsLoaded = false; } isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; @@ -276,6 +278,12 @@ class MyAppointmentsViewModel extends ChangeNotifier { getPatientAppointmentQueueDetails(); } } + + // 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(); @@ -323,6 +331,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { if (!isForTimeLine) { patientAllArrivedAppointmentsHistoryList = apiResponse.data!; isArrivedAppointmentDataLoaded = true; + isFullArrivedAppointmentsLoaded = true; } notifyListeners(); if (onSuccess != null) { @@ -354,6 +363,49 @@ class MyAppointmentsViewModel extends ChangeNotifier { 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) { + 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; + + // 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; diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 3512472d..5606c935 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -45,8 +45,10 @@ class _MyAppointmentsPageState extends State { scheduleMicrotask(() { if (!myAppointmentsViewModel.isMyAppointmentsLoading) { myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); } + // Load full arrived appointments if not already loaded + myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded(); }); super.initState(); } diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index eea9ec10..537a7dd8 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -176,6 +176,7 @@ class _WalletPaymentConfirmPageState extends State { ); }); }, onError: (err) { + LoaderBottomSheet.showLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: err.toString()), diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 2df8f77c..334e60e9 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -154,7 +154,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(); @@ -212,7 +212,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(); 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 Date: Mon, 10 Aug 2026 18:03:55 +0300 Subject: [PATCH 22/73] fized the symptoms zoom issue --- .../organ_selector_screen.dart | 257 +++++++++++++----- .../widgets/interactive_body_widget.dart | 30 +- lib/services/dialog_service.dart | 18 +- lib/theme/app_theme.dart | 14 +- 4 files changed, 224 insertions(+), 95 deletions(-) diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 2244ecbd..438d9728 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -29,11 +29,19 @@ class OrganSelectorPage extends StatefulWidget { 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/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/theme/app_theme.dart b/lib/theme/app_theme.dart index c676819f..f4465f7f 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -15,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], @@ -51,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], @@ -63,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, From 7dbb1dce3f3d627623498a0db9a6a2f92eda03f3 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 10 Aug 2026 18:07:34 +0300 Subject: [PATCH 23/73] crashlytics fixes --- .../profile_picture_view_model.dart | 30 ++++--------------- lib/presentation/home/landing_page.dart | 29 ++++++++++++++---- 2 files changed, 30 insertions(+), 29 deletions(-) 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/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b3ba5e78..60d55fb8 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(() { @@ -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); + }, ); } }, @@ -1458,6 +1476,7 @@ class _LandingPageState extends State { } openLastRating() { + if (!mounted) return; showCommonBottomSheetWithoutHeight( context, titleWidget: Selector( From d971e9d0b41b7ac3f8cf7c5351c1b815ce9b34f6 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 19:22:44 +0300 Subject: [PATCH 24/73] Crashlytics fixes & updates --- assets/langs/en-US.json | 2 +- lib/core/api/api_client.dart | 4 +- lib/core/utils/request_utils.dart | 106 +++++++++++------- .../book_appointments_view_model.dart | 3 +- .../my_appointments_view_model.dart | 35 +++--- .../prescriptions_view_model.dart | 4 +- .../appointment_payment_page.dart | 14 ++- .../appointments/my_appointments_page.dart | 12 +- .../book_appointment/select_clinic_page.dart | 1 + .../book_appointment/select_doctor_page.dart | 5 +- .../book_appointment/widgets/doctor_card.dart | 8 +- .../widgets/preferred_language_widget.dart | 2 +- .../widgets/update_email_widget.dart | 2 +- lib/widgets/common_bottom_sheet.dart | 3 +- 14 files changed, 122 insertions(+), 79 deletions(-) diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 69e0d66d..1f006d2c 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", diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 3a5ff281..ebac476d 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1307867; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 3310954; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; 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/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 8ec370a4..4831901d 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -676,7 +676,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { (failure) async { isDoctorsListLoading = false; if (onError != null) onError(LocaleKeys.noDoctorFound.tr()); - notifyListeners(); }, (apiResponse) { @@ -691,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/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 88fb8e64..293dec71 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -78,7 +78,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientUpcomingAppointmentsHistoryList = []; List patientArrivedAppointmentsHistoryList = []; - List patientAllArrivedAppointmentsHistoryList = []; + // List patientAllArrivedAppointmentsHistoryList = []; List patientMyDoctorsList = []; @@ -174,7 +174,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); - patientAllArrivedAppointmentsHistoryList.clear(); + // patientAllArrivedAppointmentsHistoryList.clear(); patientEyeMeasurementsAppointmentsHistoryList.clear(); isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; @@ -290,7 +290,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsByHospital.clear(); patientAppointmentsViewList.clear(); - patientAllArrivedAppointmentsHistoryList.clear(); + // patientAllArrivedAppointmentsHistoryList.clear(); filteredAppointmentList.clear(); patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); @@ -329,7 +329,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { isMyAppointmentsLoading = false; isAppointmentDataToBeLoaded = false; if (!isForTimeLine) { - patientAllArrivedAppointmentsHistoryList = apiResponse.data!; + // patientAllArrivedAppointmentsHistoryList = apiResponse.data!; isArrivedAppointmentDataLoaded = true; isFullArrivedAppointmentsLoaded = true; } @@ -383,7 +383,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { // Handle error } else if (apiResponse.messageStatus == 1) { patientArrivedAppointmentsHistoryList = apiResponse.data!; - patientAllArrivedAppointmentsHistoryList = apiResponse.data!; + // patientAllArrivedAppointmentsHistoryList = apiResponse.data!; isArrivedAppointmentDataLoaded = true; isFullArrivedAppointmentsLoaded = true; @@ -414,9 +414,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); @@ -853,15 +853,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; @@ -887,11 +887,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; diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index cdd85b9f..d75b3f8e 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -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) { diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 0d864b64..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; + }); + } } } }); diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 5606c935..9bd9b61d 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -48,7 +48,7 @@ class _MyAppointmentsPageState extends State { myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); } // Load full arrived appointments if not already loaded - myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded(); + // myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded(); }); super.initState(); } @@ -71,9 +71,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(() { @@ -82,6 +82,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) { @@ -152,7 +156,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/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index e0a810ed..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( diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 8813da07..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,8 +57,7 @@ class _SelectDoctorPageState extends State { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { - bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); - bookAppointmentsViewModel.getDoctorsList(isNearest: true); + bookAppointmentsViewModel.getDoctorsList(isNearest: false); } } }); diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 180edae0..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), 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/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index c86bc516..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, From 20c47d6af095554bfc2fc7b3db65b0c1042dcc37 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 19:24:37 +0300 Subject: [PATCH 25/73] updates --- ios/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From dc6b71a4ca84a00e40e6b0a3accd79c78931718e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 20:52:58 +0300 Subject: [PATCH 26/73] Tab changed to arrived in my appointments --- .../my_appointments_view_model.dart | 24 +++++++++++++++++-- .../appointments/my_appointments_page.dart | 3 +-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 293dec71..57421ee2 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -363,11 +363,25 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + changeTabToArrived() { + if (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; } @@ -394,8 +408,14 @@ class MyAppointmentsViewModel extends ChangeNotifier { isMyAppointmentsLoading = false; - // Update filtered list based on current tab - updateListWRTTab(selectedTabIndex); + // 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) { diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 9bd9b61d..82696f69 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -47,8 +47,7 @@ class _MyAppointmentsPageState extends State { myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); } - // Load full arrived appointments if not already loaded - // myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded(); + myAppointmentsViewModel.changeTabToArrived(); }); super.initState(); } From 0043e2917de089adf966cb226725e9937ac40c89 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 21:12:57 +0300 Subject: [PATCH 27/73] updates --- lib/features/my_appointments/my_appointments_view_model.dart | 2 +- lib/presentation/appointments/my_appointments_page.dart | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 57421ee2..832d94fe 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -364,7 +364,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } changeTabToArrived() { - if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { + if (isFullArrivedAppointmentsLoaded && patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { selectedTabIndex = 1; updateListWRTTab(1); notifyListeners(); diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 82696f69..da20e183 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -47,7 +47,7 @@ class _MyAppointmentsPageState extends State { myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); } - myAppointmentsViewModel.changeTabToArrived(); + // myAppointmentsViewModel.changeTabToArrived(); }); super.initState(); } diff --git a/pubspec.yaml b/pubspec.yaml index 1685f8df..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.42+43 -version: 0.0.12+1 +version: 0.0.44+45 +#version: 0.0.14+1 environment: sdk: ">=3.6.0 <4.0.0" From 0dea7414d92dbff7d00f4cc4c10f3086623f89e4 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 12 Aug 2026 14:21:43 +0300 Subject: [PATCH 28/73] updates --- lib/core/api_consts.dart | 4 +- .../appointment_details_page.dart | 4 +- .../dropdown/country_dropdown_widget.dart | 193 ++++++++++++++---- 3 files changed, 151 insertions(+), 50 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9fb7050a..865b7e75 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -911,8 +911,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/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/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 From 814b0ccd58c69d14eed415fa2a610808116fdcec Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 13 Aug 2026 10:45:33 +0300 Subject: [PATCH 29/73] Updates --- lib/core/utils/utils.dart | 39 +++++++++++++++++-- .../my_appointments_view_model.dart | 6 ++- .../medical_file/medical_file_page.dart | 2 +- lib/services/livecare_permission_service.dart | 6 +-- lib/splashPage.dart | 5 ++- pubspec.yaml | 7 ++-- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 320bd1cf..6d04fbc9 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -1057,9 +1057,42 @@ class Utils { return isHavePrivilege; } - static void openWebView({required String url}) { - Uri uri = Uri.parse(url); - launchUrl(uri, mode: LaunchMode.inAppBrowserView); + static Future openWebView({required String url}) async { + try { + Uri uri = Uri.parse(url); + + // Validate URL scheme for in-app browser + if (!uri.hasScheme || (!uri.scheme.startsWith('http'))) { + throw 'Invalid URL scheme. In-app browser only supports HTTP/HTTPS URLs'; + } + + // Check if URL can be launched + if (await canLaunchUrl(uri)) { + final launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + + if (!launched) { + // Fallback to external browser + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } else { + // Fallback to external browser + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (e) { + debugPrint('❌ Failed to open URL: $url - Error: $e'); + + // Try external browser as last resort + try { + final uri = Uri.parse(url); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (e2) { + debugPrint('❌ Failed to open URL in external browser: $e2'); + // Optionally show user-friendly error message + } + } } static Color getCardBorderColor(int currentQueueStatus) { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 832d94fe..b153c556 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -771,7 +771,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; - isMyDoctorsDataToBeLoaded = false; + + if (!isTop8) { + isMyDoctorsDataToBeLoaded = false; + } + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index cdee10db..cb3f7072 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1063,7 +1063,7 @@ class _MedicalFilePageState extends State { ], ).onPress(() { // myAppointmentsViewModel.getPatientMyDoctors(); - myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); + // myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), diff --git a/lib/services/livecare_permission_service.dart b/lib/services/livecare_permission_service.dart index bc5f2eef..52a4cc24 100644 --- a/lib/services/livecare_permission_service.dart +++ b/lib/services/livecare_permission_service.dart @@ -31,14 +31,14 @@ class LiveCarePermissionService { Permission.camera, Permission.microphone, Permission.notification, - if (Platform.isAndroid) Permission.systemAlertWindow, + // if (Platform.isAndroid) Permission.systemAlertWindow, ] : [ // Permission.camera, // Permission.microphone, Permission.notification, - if (Platform.isAndroid) Permission.systemAlertWindow, - ]; + // if (Platform.isAndroid) Permission.systemAlertWindow, + ]; try { final statuses = await permissions.request(); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index dc49428a..7407ffec 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; @@ -71,10 +72,10 @@ class _SplashScreenState extends State { } else { if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - Navigator.of(context).pushReplacement(FadePage(page: OnboardingScreen())); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); } else { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); } } }); diff --git a/pubspec.yaml b/pubspec.yaml index 9a05eb01..6ca96768 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.44+45 -#version: 0.0.14+1 +#version: 0.0.45+46 +version: 0.0.14+3 environment: sdk: ">=3.6.0 <4.0.0" @@ -63,7 +63,8 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - flutter_zoom_videosdk: 2.1.10 + # flutter_zoom_videosdk: 2.1.10 + flutter_zoom_videosdk: ^2.5.10 dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 equatable: ^2.0.7 From 636610accea6c8882772cf5565f4299cfbbb53da Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 13 Aug 2026 16:46:19 +0300 Subject: [PATCH 30/73] Updates for Zoom --- lib/core/api/api_client.dart | 2 +- lib/core/utils/zoom_config.dart | 6 + lib/presentation/home/landing_page.dart | 17 + .../tele_consultation/zoom/call_screen.dart | 40 +- .../date_range_calender.dart | 354 ++++++++++-------- .../viewmodel/date_range_calendar_model.dart | 14 +- pubspec.yaml | 2 +- 7 files changed, 258 insertions(+), 177 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index ebac476d..4d8d1650 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 3310954; + // body['PatientID'] = 945786; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/utils/zoom_config.dart b/lib/core/utils/zoom_config.dart index 09370c00..8262e83d 100644 --- a/lib/core/utils/zoom_config.dart +++ b/lib/core/utils/zoom_config.dart @@ -2,3 +2,9 @@ const Map configs = { 'ZOOM_SDK_KEY': 'b9T74nhfTg-ioP9urm970A', 'ZOOM_SDK_SECRET': 'KOzmjBNXQ1f4IPHpnngfL29uZvJMufSy2Fk8', }; + + +// const Map configs = { +// 'ZOOM_SDK_KEY': 'jYHoRpSUMTTefOwLq94Kyf1Kak513TUHpu78', +// 'ZOOM_SDK_SECRET': 'MOqi1zc18VFOMEaBDshFSHKLB0C2n9M0K48H', +// }; diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 3caed75a..db300d98 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -58,6 +58,7 @@ import 'package:hmg_patient_app_new/presentation/notifications/notifications_lis import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discounts.dart'; import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; @@ -291,6 +292,15 @@ class _LandingPageState extends State { page: FamilyMedicalScreen(), ), ); + + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("yosemite-338", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); }, name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), imageWidget: Selector( @@ -314,6 +324,13 @@ class _LandingPageState extends State { onPressed: () async { await authVM.onLoginPressed(); + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // arguments: CallArguments("lake-tahoe-289", "123", "Patient", "40", "0", true, 1), + // ); + // Navigator.of(context).push( // CustomPageRoute( // // page: NotificationsListPage(), diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index 64c6c471..1c9db67a 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -84,7 +84,7 @@ class _CallScreenState extends State { //hide status bar SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); - var circleButtonSize = 65.0; + var circleButtonSize = 65.h; Color backgroundColor = const Color(0xFF232323); Color buttonBackgroundColor = const Color.fromRGBO(0, 0, 0, 0.6); Color chatTextColor = const Color(0xFFAAAAAA); @@ -178,12 +178,12 @@ class _CallScreenState extends State { // "Join", // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), // ); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); }); final sessionNeedPasswordListener = eventListener.addListener(EventType.onSessionNeedPassword, (data) async { @@ -1773,16 +1773,17 @@ class _CallScreenState extends State { alignment: Alignment.centerRight, child: FractionallySizedBox( widthFactor: 0.2, - heightFactor: 0.6, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + heightFactor: 0.8, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: onPressAudio, - icon: isMuted.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/unmute@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/mute@2x.png"), - iconSize: circleButtonSize, - tooltip: isMuted.value == true ? "Unmute" : "Mute", - ), + icon: isMuted.value + ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/unmute@2x.png", width: circleButtonSize.h, height: circleButtonSize.h) + : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/mute@2x.png", width: circleButtonSize.h, height: circleButtonSize.h), + iconSize: circleButtonSize, + ), // IconButton( // onPressed: onPressShare, // icon: isSharing.value ? Image.asset("assets/images/png/zoom/share-off@2x.png") : Image.asset("assets/images/png/zoom/share-on@2x.png"), @@ -1791,9 +1792,11 @@ class _CallScreenState extends State { IconButton( onPressed: onPressVideo, iconSize: circleButtonSize, - icon: isVideoOn.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-off@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-on@2x.png"), - ), - Column( + icon: isVideoOn.value + ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-off@2x.png", width: circleButtonSize.h, height: circleButtonSize.h) + : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-on@2x.png", width: circleButtonSize.h, height: circleButtonSize.h), + ), + Column( children: [ IconButton( onPressed: () async { @@ -1808,7 +1811,8 @@ class _CallScreenState extends State { ), ], ), - )), + ), + ), // Container( // margin: const EdgeInsets.only(left: 16, right: 16, bottom: 40, top: 10), // alignment: Alignment.bottomCenter, diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 463f516a..a544f010 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -56,8 +56,9 @@ class _DateRangeSelectorState extends State { late DateRangeSelectorRangeViewModel model; PickerViewMode _viewMode = PickerViewMode.date; - // Cache for Hijri conversions to avoid repeated calculations - final Map _hijriCache = {}; + // Prevent Syncfusion's initial onViewChanged callback from scheduling an + // unnecessary second build of the same month. + late int _displayedMonthKey; // Track the current Hijri month/year being displayed (for header display accuracy) HijriGregDate? _currentHijriDisplay; @@ -65,10 +66,14 @@ class _DateRangeSelectorState extends State { @override void initState() { _calendarController = DateRangePickerController(); + final today = DateTime.now(); + _displayedMonthKey = _monthKey(today); + if (widget.designType == CalendarDesignType.designV2) { + _calendarController.displayDate = today; + } scheduleMicrotask(() { if (widget.designType == CalendarDesignType.designV2) { // For V2, select today's date by default - final today = DateTime.now(); _calendarController.selectedDate = today; model.updateSelectedDate(today); } else { @@ -83,7 +88,9 @@ class _DateRangeSelectorState extends State { Widget build(BuildContext context) { model = Provider.of(context); - _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); + if (widget.designType != CalendarDesignType.designV2) { + _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); + } return widget.designType == CalendarDesignType.designV2 ? _buildDesignV2(widget.btnTitle) : _buildDefaultUI(); } @@ -273,18 +280,17 @@ class _DateRangeSelectorState extends State { tabs: [CustomTabBarModel(null, LocaleKeys.gregorianDate.tr()), CustomTabBarModel(null, LocaleKeys.hijriDate.tr())], onTabChange: (index) { final calendarModel = Provider.of(context, listen: false); + if (calendarModel.getSelectedTabIndex == index) return; + + // Update local fields before notifying the provider so + // the calendar changes with a single rebuild. + _viewMode = PickerViewMode.date; + _currentHijriDisplay = null; calendarModel.setTabIndex(index); // Notify parent widget about calendar type change (e.g., AuthenticationViewModel) final isGregorian = index == 0; widget.onCalendarTypeChanged?.call(isGregorian); - print('📅 Calendar type changed: ${isGregorian ? "Gregorian" : "Hijri"}'); - - // Reset view mode and Hijri display when switching calendar types - setState(() { - _viewMode = PickerViewMode.date; - _currentHijriDisplay = null; - }); }, ), ), @@ -306,12 +312,17 @@ class _DateRangeSelectorState extends State { child: Material( color: AppColors.whiteColor, // Rebuild when view mode or calendar type changes + // Syncfusion caches its rendered date cells. Include + // the calendar type in the key so Gregorian/Hijri day + // labels refresh immediately when the tab changes. + // Hijri conversion is now constant-time, so this + // targeted recreation remains responsive. key: ValueKey('${calendarModel.getSelectedTabIndex}-$_viewMode'), child: _viewMode == PickerViewMode.month ? _buildMonthPicker(isArabic, calendarModel) : _viewMode == PickerViewMode.year - ? _buildYearPicker(isArabic, calendarModel) - : (calendarModel.isHijri ? _buildHijriCalendar(isArabic) : _buildGregorianCalendar(isArabic)), + ? _buildYearPicker(isArabic, calendarModel) + : (calendarModel.isHijri ? _buildHijriCalendar(isArabic) : _buildGregorianCalendar(isArabic)), ), ), ], @@ -343,6 +354,8 @@ class _DateRangeSelectorState extends State { // Build Gregorian calendar (Design V2 - Single Date Selection) Widget _buildGregorianCalendar(bool isArabic) { + final today = DateTime.now(); + return SfDateRangePicker( controller: _calendarController, selectionMode: DateRangePickerSelectionMode.single, @@ -366,7 +379,7 @@ class _DateRangeSelectorState extends State { // Custom cell builder for square border selection cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { if (cellDetails.date != DateTime(0)) { - final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; + final isToday = cellDetails.date.day == today.day && cellDetails.date.month == today.month && cellDetails.date.year == today.year; final isSelected = _calendarController.selectedDate != null && cellDetails.date.day == _calendarController.selectedDate!.day && cellDetails.date.month == _calendarController.selectedDate!.month && @@ -414,12 +427,7 @@ class _DateRangeSelectorState extends State { ), ), onViewChanged: (DateRangePickerViewChangedArgs args) { - // Trigger rebuild when month changes - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - } - }); + _handleCalendarViewChanged(args); }, onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { if (args.value is DateTime) { @@ -433,105 +441,155 @@ class _DateRangeSelectorState extends State { ); } - // Build Hijri calendar (Design V2 - Single Date Selection) - // Note: SfDateRangePicker doesn't have built-in Hijri support - // Using Gregorian calendar with Hijri date conversion in the model + // Build one complete Hijri month instead of relabelling the dates in a + // Gregorian month. A Gregorian month overlaps two Hijri months, which would + // otherwise produce sequences such as 16...30, 1...17 in the same grid. Widget _buildHijriCalendar(bool isArabic) { final calendarModel = Provider.of(context, listen: false); + final today = DateTime.now(); + final displayedGregorian = _calendarController.displayDate ?? today; + final displayedHijri = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedGregorian); + final firstHijriDay = HijriGregDate(day: 1, month: displayedHijri.month, year: displayedHijri.year); + final firstGregorianDay = calendarModel.hijriToGregorian(firstHijriDay); + final leadingEmptyCells = firstGregorianDay.weekday % DateTime.daysPerWeek; + final daysInMonth = calendarModel.getDaysInMonth(displayedHijri.year, displayedHijri.month); + final selectedDate = _calendarController.selectedDate; + const weekdays = [ + DateTime.sunday, + DateTime.monday, + DateTime.tuesday, + DateTime.wednesday, + DateTime.thursday, + DateTime.friday, + DateTime.saturday, + ]; - return SfDateRangePicker( - controller: _calendarController, - selectionMode: DateRangePickerSelectionMode.single, - showNavigationArrow: false, - headerHeight: 0, - backgroundColor: AppColors.whiteColor, - monthViewSettings: DateRangePickerMonthViewSettings( - viewHeaderStyle: DateRangePickerViewHeaderStyle( - backgroundColor: AppColors.whiteColor, - textStyle: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w600, - letterSpacing: -0.46, - color: AppColors.textColor, + return Column( + children: [ + SizedBox( + height: 32.h, + child: Row( + children: weekdays + .map( + (weekday) => Expanded( + child: Center( + child: Text( + calendarModel.getWeekdayNameLocalized(weekday, isArabic), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.textColor, + ), + ), + ), + ), + ) + .toList(), ), ), - showTrailingAndLeadingDates: false, - dayFormat: "EEE", - ), - cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { - if (cellDetails.date != DateTime(0)) { - // Use cached Hijri conversion - final dateKey = '${cellDetails.date.year}-${cellDetails.date.month}-${cellDetails.date.day}'; - final hijriDate = _hijriCache.putIfAbsent(dateKey, () => calendarModel.gregorianToHijri(cellDetails.date)); + Expanded( + child: GridView.builder( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: DateTime.daysPerWeek, + mainAxisExtent: 36.h, + mainAxisSpacing: 4.h, + ), + itemCount: 42, + itemBuilder: (context, index) { + final hijriDay = index - leadingEmptyCells + 1; + if (hijriDay < 1 || hijriDay > daysInMonth) { + return const SizedBox.shrink(); + } - final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; - final isSelected = _calendarController.selectedDate != null && - cellDetails.date.day == _calendarController.selectedDate!.day && - cellDetails.date.month == _calendarController.selectedDate!.month && - cellDetails.date.year == _calendarController.selectedDate!.year; + final hijriDate = HijriGregDate(day: hijriDay, month: displayedHijri.month, year: displayedHijri.year); + final gregorianDate = calendarModel.hijriToGregorian(hijriDate); + final isToday = _isSameDate(gregorianDate, today); + final isSelected = selectedDate != null && _isSameDate(gregorianDate, selectedDate); - return Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.h), - border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, - color: Colors.transparent, - ), - child: Text( - hijriDate.day.toString(), - style: TextStyle( - fontFamily: "Poppins", - fontSize: 12.f, - color: AppColors.textColor, - fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), - ), - ), - ); - } - return Container(); - }, - selectionShape: DateRangePickerSelectionShape.rectangle, - selectionRadius: 8.h, - selectionColor: Colors.transparent, - selectionTextStyle: TextStyle( - fontFamily: "Poppins", - color: AppColors.textColor, - fontWeight: FontWeight.w600, - ), - todayHighlightColor: Colors.transparent, - monthCellStyle: DateRangePickerMonthCellStyle( - textStyle: TextStyle( - fontFamily: "Poppins", - fontSize: 12.f, - color: AppColors.textColor, - ), - todayTextStyle: TextStyle( - fontFamily: "Poppins", - color: AppColors.textColor, - fontWeight: FontWeight.bold, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _calendarController.selectedDate = gregorianDate; + setState(() { + start = gregorianDate; + end = gregorianDate; + }); + model.updateSelectedDate(gregorianDate); + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Text( + hijriDay.toString(), + style: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), + ), + ), + ), + ); + }, + ), ), - ), - onViewChanged: (DateRangePickerViewChangedArgs args) { - // Clear cache when month changes to avoid stale data - _hijriCache.clear(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - } - }); - }, - onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { - if (args.value is DateTime) { - setState(() { - start = args.value; - end = args.value; - }); - model.updateSelectedDate(args.value); - } - }, + ], ); } + bool _isSameDate(DateTime first, DateTime second) { + return first.year == second.year && first.month == second.month && first.day == second.day; + } + + int _monthKey(DateTime date) => (date.year * 100) + date.month; + + void _changeHijriMonth(DateRangCalenderModel calendarModel, int monthDelta) { + final displayedDate = _calendarController.displayDate ?? DateTime.now(); + final currentHijri = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); + final zeroBasedMonth = (currentHijri.year * 12) + currentHijri.month - 1 + monthDelta; + final nextHijriDate = HijriGregDate( + day: 1, + month: (zeroBasedMonth % 12) + 1, + year: zeroBasedMonth ~/ 12, + ); + final nextGregorianDate = calendarModel.hijriToGregorian(nextHijriDate); + + setState(() { + _currentHijriDisplay = nextHijriDate; + _calendarController.displayDate = nextGregorianDate; + _displayedMonthKey = _monthKey(nextGregorianDate); + }); + } + + void _handleCalendarViewChanged(DateRangePickerViewChangedArgs args) { + final rangeStart = args.visibleDateRange.startDate; + final rangeEnd = args.visibleDateRange.endDate; + final displayedDate = _calendarController.displayDate ?? + (rangeEnd == null + ? rangeStart + : rangeStart?.add(Duration(days: rangeEnd.difference(rangeStart).inDays ~/ 2))); + + if (displayedDate == null) return; + + final newMonthKey = _monthKey(displayedDate); + if (newMonthKey == _displayedMonthKey) return; + + _displayedMonthKey = newMonthKey; + _currentHijriDisplay = null; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + } + // Build unified calendar header for both Gregorian and Hijri Widget _buildCalendarHeader(bool isArabic, DateRangCalenderModel calendarModel) { final displayedDate = _calendarController.displayDate ?? DateTime.now(); @@ -609,12 +667,10 @@ class _DateRangeSelectorState extends State { icon: Icon(Icons.chevron_left, color: AppColors.primaryRedColor), onPressed: () { if (_viewMode == PickerViewMode.date) { - _calendarController.backward!(); - // Update tracked Hijri display when navigating months if (calendarModel.isHijri) { - setState(() { - _currentHijriDisplay = null; // Will recalculate on next build - }); + _changeHijriMonth(calendarModel, -1); + } else { + _calendarController.backward!(); } } else if (_viewMode == PickerViewMode.year) { // Navigate years backward by 12 @@ -625,7 +681,7 @@ class _DateRangeSelectorState extends State { try { final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year - 12); final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); - _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _calendarController.displayDate = newGregorianDate; _currentHijriDisplay = newHijriDate; } catch (e) { // Fallback @@ -645,12 +701,10 @@ class _DateRangeSelectorState extends State { icon: Icon(Icons.chevron_right, color: AppColors.primaryRedColor), onPressed: () { if (_viewMode == PickerViewMode.date) { - _calendarController.forward!(); - // Update tracked Hijri display when navigating months if (calendarModel.isHijri) { - setState(() { - _currentHijriDisplay = null; // Will recalculate on next build - }); + _changeHijriMonth(calendarModel, 1); + } else { + _calendarController.forward!(); } } else if (_viewMode == PickerViewMode.year) { // Navigate years forward by 12 @@ -661,7 +715,7 @@ class _DateRangeSelectorState extends State { try { final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year + 12); final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); - _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _calendarController.displayDate = newGregorianDate; _currentHijriDisplay = newHijriDate; } catch (e) { // Fallback @@ -696,7 +750,7 @@ class _DateRangeSelectorState extends State { if (calendarModel.isHijri) { // For Hijri calendar final hijriCurrent = calendarModel.gregorianToHijri(currentDate); - final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + final hijriDisplayed = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); currentMonth = hijriCurrent.month; currentYear = hijriCurrent.year; displayedYear = hijriDisplayed.year; @@ -730,11 +784,9 @@ class _DateRangeSelectorState extends State { try { final hijriDate = HijriGregDate(day: 1, month: monthIndex, year: displayedYear); final gregorianDate = calendarModel.hijriToGregorian(hijriDate); - _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + _calendarController.displayDate = gregorianDate; // Track the Hijri month/year for accurate header display _currentHijriDisplay = hijriDate; - // Clear cache since we changed the month - _hijriCache.clear(); } catch (e) { // Fallback if conversion fails _calendarController.displayDate = DateTime(displayedDate.year, monthIndex, 1); @@ -784,7 +836,7 @@ class _DateRangeSelectorState extends State { if (calendarModel.isHijri) { // For Hijri calendar - final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + final hijriDisplayed = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); final hijriCurrent = calendarModel.gregorianToHijri(DateTime.now()); displayedYear = hijriDisplayed.year; currentDisplayYear = hijriCurrent.year; @@ -818,7 +870,7 @@ class _DateRangeSelectorState extends State { final currentHijriMonth = _currentHijriDisplay?.month ?? calendarModel.gregorianToHijri(displayedDate).month; final hijriDate = HijriGregDate(day: 1, month: currentHijriMonth, year: year); final gregorianDate = calendarModel.hijriToGregorian(hijriDate); - _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + _calendarController.displayDate = gregorianDate; // Track the Hijri month/year for accurate header display _currentHijriDisplay = hijriDate; } catch (e) { @@ -876,34 +928,34 @@ class _DateRangeSelectorState extends State { } displayDate(String label, String? date, bool isNotSelected) => Expanded( - child: Row( - spacing: 12.h, + child: Row( + spacing: 12.h, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rangeCalendar, iconColor: isNotSelected ? AppColors.borderOnlyColor : AppColors.blackColor, height: 24, width: 24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rangeCalendar, iconColor: isNotSelected ? AppColors.borderOnlyColor : AppColors.blackColor, height: 24, width: 24), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: AppColors.inputLabelTextColor, - fontSize: 12.f, - fontWeight: FontWeight.w600, - ), - ), - Text( - date!, - style: TextStyle( - color: AppColors.textColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - ), - ) - ], + Text( + label, + style: TextStyle( + color: AppColors.inputLabelTextColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + Text( + date!, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), ) ], - ), - ); + ) + ], + ), + ); selectionChip(DateRangeSelectorRangeViewModel model) { return Row( diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart index 54c23a72..50b416cd 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart @@ -122,12 +122,12 @@ class HijriGregConverter { static int _hijriYearStartJulian(int hijriYear) { if (hijriYear <= 1) return _hijriEpoch; - int totalDays = 0; - for (int year = 1; year < hijriYear; year++) { - totalDays += _hijriYearLength(year); - } - - return _hijriEpoch + totalDays; + // A Hijri year has 354 days, with 11 leap days in every 30-year + // cycle. Calculate the completed years directly instead of iterating + // from year 1 for every calendar cell. + final completedYears = hijriYear - 1; + final completedLeapDays = (3 + (11 * hijriYear)) ~/ 30; + return _hijriEpoch + (completedYears * 354) + completedLeapDays; } static int _hijriYearLength(int year) { @@ -358,6 +358,8 @@ class DateRangCalenderModel extends ChangeNotifier { } void setTabIndex(int index) { + if (_selectedTabIndex == index) return; + _selectedTabIndex = index; _calendarType = index == 0 ? CalendarType.gregorian : CalendarType.hijri; // Persist the selection for next time the widget opens diff --git a/pubspec.yaml b/pubspec.yaml index 6ca96768..4c6b92b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - # flutter_zoom_videosdk: 2.1.10 +# flutter_zoom_videosdk: 2.1.10 flutter_zoom_videosdk: ^2.5.10 dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 From 2a8e9ec2fb76837486e2af1ea2475d092726505b Mon Sep 17 00:00:00 2001 From: taha Date: Sun, 16 Aug 2026 15:19:24 +0300 Subject: [PATCH 31/73] fix: remove null safety operator from doctor image URLs --- lib/core/utils/utils.dart | 9 +- .../widgets/appointment_card.dart | 2 +- .../doctor_row_appointment_rating.dart | 2 +- .../widgets/ask_doctor_appointment_card.dart | 2 +- .../book_appointment_page.dart | 4 +- .../book_appointment/doctor_profile_page.dart | 4 +- .../contact_us/feedback_page.dart | 2 +- .../insurance_approval_details_page.dart | 2 +- .../widgets/insurance_approval_card.dart | 2 +- .../lab/lab_result_item_view.dart | 2 +- .../medical_file/medical_file_page.dart | 4 +- .../patient_sickleaves_list_page.dart | 2 +- .../widgets/patient_sick_leave_card.dart | 2 +- .../widgets/patient_medical_report_card.dart | 2 +- .../offers_and_discounts_detailed_page.dart | 2 +- .../offers_and_discounts_page.dart | 2 +- ...scription_delivery_order_summary_page.dart | 2 +- .../prescription_detail_page.dart | 2 +- .../prescriptions/prescription_item_view.dart | 2 +- .../prescriptions_list_page.dart | 2 +- .../rate_appointment/widget/doctor_row.dart | 2 +- pubspec.lock | 98 ++++++++++++++++--- 22 files changed, 116 insertions(+), 37 deletions(-) diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 320bd1cf..c69c2146 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.no_visit_icon, + ); + } final iconH = height ?? 24.h; final iconW = width ?? 24.w; return PreserveImageColors( diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 91ebaa14..1cbe3be2 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -232,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, 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/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/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index cb93d9e0..c8fcdb36 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -196,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, @@ -389,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, diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 9b5d6583..5b6ad09d 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -89,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, @@ -116,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, diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 251c720d..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, diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index d0d7f681..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, 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/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/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index cdee10db..fbc687cc 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -926,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, @@ -1127,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/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/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/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/rate_appointment/widget/doctor_row.dart b/lib/presentation/rate_appointment/widget/doctor_row.dart index 09b6c134..a69b90df 100644 --- a/lib/presentation/rate_appointment/widget/doctor_row.dart +++ b/lib/presentation/rate_appointment/widget/doctor_row.dart @@ -25,7 +25,7 @@ class BuildDoctorRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Utils.buildImgWithNetwork( - url: isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL , + url: isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails?.projectID}.jpg' : appointmentDetails?.doctorImageURL , iconColor: AppColors.transparent, width: 63.h, height: 63.h, diff --git a/pubspec.lock b/pubspec.lock index 4f2c35ac..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: @@ -499,6 +507,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_amazonpaymentservices: + dependency: "direct main" + description: + name: flutter_amazonpaymentservices + sha256: "23f45fd3b1e12a0933da3704d66cebd19c4c41e28a50d4fd458d3e3e6311f332" + url: "https://pub.dev" + source: hosted + version: "0.0.13" flutter_cache_manager: dependency: transitive description: @@ -700,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: @@ -720,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: @@ -1341,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: @@ -1641,6 +1665,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + screen_brightness: + dependency: "direct main" + description: + name: screen_brightness + sha256: "7d4ac84ae26b37c01d6f5db7123a72db7933e1f2a2a8c369a51e08f81b3178d8" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_android: + dependency: transitive + description: + name: screen_brightness_android + sha256: "8c69d3ac475e4d625e7fa682a3a51a69ff59abe5b4a9e57f6ec7d830a6c69bd6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_ios: + dependency: transitive + description: + name: screen_brightness_ios + sha256: f08f70ca1ac3e30719764b5cfb8b3fe1e28163065018a41b3e6f243ab146c2f1 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_macos: + dependency: transitive + description: + name: screen_brightness_macos + sha256: "70c2efa4534e22b927e82693488f127dd4a0f008469fccf4f0eefe9061bbdd6a" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_platform_interface: + dependency: transitive + description: + name: screen_brightness_platform_interface + sha256: "9f3ebf7f22d5487e7676fe9ddaf3fc55b6ff8057707cf6dc0121c7dfda346a16" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_windows: + dependency: transitive + description: + name: screen_brightness_windows + sha256: c8e12a91cf6dd912a48bd41fcf749282a51afa17f536c3460d8d05702fb89ffa + url: "https://pub.dev" + source: hosted + version: "1.0.1" scrollable_positioned_list: dependency: "direct main" description: @@ -1890,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: @@ -2175,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" From f2bf8edeb2f6d418c0edb4459f98c2ad53d59f9a Mon Sep 17 00:00:00 2001 From: taha Date: Sun, 16 Aug 2026 15:40:22 +0300 Subject: [PATCH 32/73] feat: add no image found icon and update references --- assets/images/svg/no_image_found.svg | 5 +++++ lib/core/app_assets.dart | 1 + lib/core/utils/utils.dart | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 assets/images/svg/no_image_found.svg 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/lib/core/app_assets.dart b/lib/core/app_assets.dart index b4611890..1aed020e 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -400,6 +400,7 @@ class AppAssets { 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/utils/utils.dart b/lib/core/utils/utils.dart index c69c2146..b4e91854 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -863,7 +863,7 @@ class Utils { return Utils.buildSvgWithAssets( width: width ?? 24.w, height: height ?? 24.h, - icon: AppAssets.no_visit_icon, + icon: AppAssets.noImageFound, ); } final iconH = height ?? 24.h; @@ -877,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); }, ), ); From 22ab9b7d2dc158da01ea3ee9276621403a5d8d14 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 16 Aug 2026 15:47:39 +0300 Subject: [PATCH 33/73] Crashlytics fixes --- .../authentication_view_model.dart | 8 +++-- .../medical_file/medical_file_view_model.dart | 12 ++++++-- lib/features/payfort/payfort_repo.dart | 30 +++++++++++++++++++ lib/features/payfort/payfort_view_model.dart | 20 +++++++++++++ .../widgets/appointment_doctor_card.dart | 13 ++++---- .../book_appointment_page.dart | 3 +- lib/widgets/in_app_browser/InAppBrowser.dart | 3 ++ lib/widgets/input_widget.dart | 3 +- 8 files changed, 76 insertions(+), 16 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 7688cfa4..72ca42cb 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -824,7 +824,11 @@ class AuthenticationViewModel extends ChangeNotifier { onSuccess: (dynamic respData) async { try { if (respData != null) { - dynamic data = await SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); + SelectDeviceByImeiRespModelElement data = SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); + if (data.mobile == null || data.mobile == "" || data.identificationNo == null || data.identificationNo == "") { + return; + } + _appState.setSelectDeviceByImeiRespModelElement(data); LoaderBottomSheet.hideLoader(); @@ -856,8 +860,6 @@ class AuthenticationViewModel extends ChangeNotifier { } Future checkUserAuthentication({required OTPTypeEnum otpTypeEnum, Function(dynamic)? onSuccess, Function(String)? onError}) async { - // TODO: THIS SHOULD BE REMOVED LATER ON AND PASSED FROM APP STATE DIRECTLY INTO API CLIENT. BECAUSE THIS API ONLY NEEDS FEW PARAMS FROM USER - loginTypeEnum = otpTypeEnum == OTPTypeEnum.sms ? LoginTypeEnum.sms : LoginTypeEnum.whatsapp; // if (phoneNumberController.text.isEmpty) { diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 6fb8c8a2..a9bfb09c 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -45,7 +45,6 @@ class MedicalFileViewModel extends ChangeNotifier { List patientSickLeavesViewList = []; bool isSickLeavesSortByClinic = true; - bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -61,6 +60,7 @@ class MedicalFileViewModel extends ChangeNotifier { List patientMedicalReportsViewList = []; bool isMedicalReportsSortByClinic = true; + bool isMedicalReportsDataNeedsReloading = true; List patientMedicalReportAppointmentHistoryList = []; PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; @@ -192,7 +192,7 @@ class MedicalFileViewModel extends ChangeNotifier { } setIsPatientMedicalReportsLoading(bool val) { - if (val) { + if (val && isMedicalReportsDataNeedsReloading) { onMedicalReportTabChange(0); patientMedicalReportList.clear(); patientMedicalReportsByClinic.clear(); @@ -200,8 +200,8 @@ class MedicalFileViewModel extends ChangeNotifier { patientMedicalReportsViewList.clear(); patientMedicalReportPDFBase64 = ""; isMedicalReportsSortByClinic = true; + isPatientMedicalReportsListLoading = val; } - isPatientMedicalReportsListLoading = val; notifyListeners(); } @@ -373,6 +373,10 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getPatientMedicalReportList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + if (!isMedicalReportsDataNeedsReloading) { + return; + } + patientMedicalReportList.clear(); patientMedicalReportRequestedList.clear(); patientMedicalReportReadyList.clear(); @@ -385,6 +389,7 @@ class MedicalFileViewModel extends ChangeNotifier { (failure) async => await errorHandlerService.handleError( failure: failure, onOkPressed: () { + isMedicalReportsDataNeedsReloading = true; onError!(failure.message); }, ), @@ -400,6 +405,7 @@ class MedicalFileViewModel extends ChangeNotifier { } onMedicalReportTabChange(0); isPatientMedicalReportsListLoading = false; + isMedicalReportsDataNeedsReloading = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart index 3cd27a9d..9262d662 100644 --- a/lib/features/payfort/payfort_repo.dart +++ b/lib/features/payfort/payfort_repo.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/common_models/tamara_request_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; @@ -34,6 +35,8 @@ abstract class PayfortRepo { Future>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel}); Future>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel}); + + Future>> tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel}); } class PayfortRepoImp implements PayfortRepo { @@ -350,4 +353,31 @@ class PayfortRepoImp implements PayfortRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(TAMARA_REQUEST_INSERT, body: tamaraRequestModel.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, isAllowAny: true, isPaymentServices: true); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index f567264f..031c3182 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -9,6 +9,7 @@ import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.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/common_models/tamara_request_model.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; @@ -116,6 +117,25 @@ class PayfortViewModel extends ChangeNotifier { ); } + Future tamaraRequestInsert({required TamaraRequestModel tamaraRequestModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.tamaraRequestInsert(tamaraRequestModel: tamaraRequestModel); + + 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) { + // payfortProjectDetailsRespModel = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 6e3f77d1..8738a449 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -152,16 +152,13 @@ class AppointmentDoctorCard extends StatelessWidget { ), AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - icon: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! - ? AppAssets.walkin_appointment_icon + icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, - iconColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, - labelText: patientAppointmentHistoryResponseModel.isLiveCareAppointment! - ? LocaleKeys.livecare.tr(context: context) + iconColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, + labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context), - backgroundColor: - !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor, - textColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, + backgroundColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.greyColor : AppColors.successColor, + textColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, ), ], ), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index cb93d9e0..be91951a 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -389,7 +389,8 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Utils.buildImgWithNetwork( - url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, + url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl ?? "https://eservices-test-bucket.s3.me-central-1.amazonaws.com/unkown_male.png", + // url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, iconColor: AppColors.transparent, width: 64.h, height: 64.h, diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index e8370e5b..7f4836f6 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -11,6 +11,7 @@ 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/authentication/models/resp_models/authenticated_user_resp_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; enum _PAYMENT_TYPE { PACKAGES, PHARMACY, PATIENT } @@ -217,6 +218,8 @@ class MyInAppBrowser extends InAppBrowser { tamaraRequestModel.appointmentDate = (appoDate != null && appoDate != "") ? appoDate : null; tamaraRequestModel.isSchedule = ((appoNo != null && appoNo != "") && (appoDate != null && appoDate != "")) ? true : false; + getIt.get().tamaraRequestInsert(tamaraRequestModel: tamaraRequestModel); + // service.tamaraInsertRequest(tamaraRequestModel, context).then((res) { // // if (context != null) GifLoaderDialogUtils.hideDialog(context); generateTamaraURL(amount, orderDesc, transactionID, projId, emailId, paymentMethod, patientType, patientName, patientID, authenticatedUser, isLiveCareAppo, servID, LiveServID, appoDate, appoNo, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index b7fe686f..b1214b87 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -198,7 +198,8 @@ class TextInputWidget extends StatelessWidget { ], ), ), - (suffix != null) ? suffix! : SizedBox.shrink() + // (suffix != null) ? suffix : SizedBox.shrink() + suffix ?? SizedBox.shrink() ], ), ), From e93771df33a081eb98811ebdfa442d0afd7de3aa Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 16 Aug 2026 17:39:29 +0300 Subject: [PATCH 34/73] Pedia Dental issue fixes --- .../book_appointments_repo.dart | 5 ++ .../appointment_via_region_viewmodel.dart | 14 ++++ .../search_doctor_by_name.dart | 66 ++++++++++++------- .../book_appointment/select_clinic_page.dart | 21 ++++-- .../book_appointment/select_doctor_page.dart | 6 +- .../widgets/appointment_calendar.dart | 21 ++++-- .../book_appointment/widgets/doctor_card.dart | 29 +++++--- 7 files changed, 122 insertions(+), 40 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index e5200f67..1e0b8739 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -2,7 +2,9 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; @@ -181,6 +183,9 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { "IsSearchAppointmnetByClinicID": isContinueDentalPlan ? false : true, "isDentalAllowedBackend": clinicID == 17 ? true : isContinueDentalPlan, "IsGetNearAppointment": isNearest, + "gender": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.gender! : 0, + "age": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.age! : 0, + "DateofBirth": getIt.get().isAuthenticated ? getIt.get().getAuthenticatedUser()!.dateofBirth! : null, if (isNearest) "SelectedDate": DateUtil.convertDateToString(DateTime.now()), "License": true }; diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 1144be82..bb4179e1 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -161,6 +161,20 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { page: DentalChiefComplaintsPage(), ), ); + } else { + if (appState.getAuthenticatedUser()!.age! > 12) { + navigationService.push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + navigationService.push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } } if (clinicId == 253) { diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 165acfbf..4e9f1435 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -3,15 +3,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/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/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/doctors_filter.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -214,28 +218,46 @@ class _SearchDoctorByNameState extends State { bookAppointmentsViewModel: bookAppointmentsViewModel, isDoctorNameSearch: true, ).paddingSymmetrical(16.h, 0.h).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[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, - ); - }, - ); + if (bookAppointmentsVM.filteredDoctorList[index].clinicID == 17 && getIt.get().getAuthenticatedUser()!.age! < 12) { + bookAppointmentsViewModel.setProjectID(bookAppointmentsVM.filteredDoctorList[index].projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( + clinicID: bookAppointmentsVM.filteredDoctorList[index].clinicID, + clinicDescription: bookAppointmentsVM.filteredDoctorList[index].clinicName, + isLiveCareClinicAndOnline: false, + liveCareServiceID: 0, + liveCareClinicID: 0)); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } else { + bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[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, + ); + }, + ); + } + // Column( // children: bookAppointmentsVM.doctorsList[index].map((entry) { // final doctorIndex = entry.key; diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index bf4b5d58..785ebb62 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1045,8 +1045,17 @@ class _SelectClinicPageState extends State { //Dental Clinic Flow if (clinic.clinicID == 17) { if (appState.isAuthenticated) { - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); - return; + if (appState.getAuthenticatedUser()!.age! > 12) { + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); + return; + } else { + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); Navigator.of(context).push( @@ -1174,8 +1183,12 @@ class _SelectClinicPageState extends State { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { bookAppointmentsViewModel.setProjectID(id); if (appState.isAuthenticated) { - initDentalAppointment(); - return SizedBox.shrink(); + if (appState.getAuthenticatedUser()!.age! > 12) { + initDentalAppointment(); + return SizedBox.shrink(); + } else { + return SizedBox.shrink(); + } } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index cad4fe0e..661c9a30 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -53,7 +53,11 @@ class _SelectDoctorPageState extends State { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { - bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + if (appState.getAuthenticatedUser()!.age! > 12) { + bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + } else { + bookAppointmentsViewModel.getDoctorsList(isNearest: false); + } } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index bc6e214c..a95989a6 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -278,11 +278,22 @@ class _AppointmentCalendarState extends State { bookAppointmentsViewModel.setProjectID(bookAppointmentsViewModel.selectedDoctor.projectID.toString()); bookAppointmentsViewModel.setSelectedClinic(selectedClinic); bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); - Navigator.of(context).push( - CustomPageRoute( - page: DentalChiefComplaintsPage(), - ), - ); + if(appState.getAuthenticatedUser()!.age! > 12) { + 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 { bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay); diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index bb5a599d..8a48465a 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -185,14 +186,26 @@ class DoctorCard extends StatelessWidget { if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 17) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); - bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); - bookAppointmentsViewModel.setSelectedClinic(selectedClinic); - bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); - Navigator.of(context).push( - CustomPageRoute( - page: DentalChiefComplaintsPage(), - ), - ); + if (getIt.get().getAuthenticatedUser()!.age! > 12) { + bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else { + bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.push( + context, + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } else if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 253) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); From 83b17532d04acb085740935e985f3cd8272d4332 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 17 Aug 2026 16:19:01 +0300 Subject: [PATCH 35/73] Security changes implemented --- .../gradle/wrapper/gradle-wrapper.properties | 3 +- lib/core/app_state.dart | 9 +- lib/core/dependencies.dart | 5 + lib/core/talsec_config.dart | 15 ++ lib/main.dart | 42 ++++- lib/services/security_service.dart | 157 ++++++++++++++++++ lib/splashPage.dart | 79 ++++++--- lib/unsafe_device.dart | 117 +++++++++++++ pubspec.yaml | 2 + 9 files changed, 401 insertions(+), 28 deletions(-) create mode 100644 lib/core/talsec_config.dart create mode 100644 lib/services/security_service.dart create mode 100644 lib/unsafe_device.dart diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ac3b4792..2299155e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +#distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index a27b727e..0d015922 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -170,7 +170,6 @@ class AppState { set setIsAuthenticated(v) => isAuthenticated = v; - String deviceTypeID = ""; set setDeviceTypeID(v) => deviceTypeID = v; @@ -179,6 +178,14 @@ class AppState { String get getFamilyFileTokenID => _familyFileTokenID; + bool isSafeDevice = true; + + // set setIsSafeDevice(v) => isSafeDevice = v; + + set setIsSafeDevice(bool value) { + isSafeDevice = value; + } + set setFamilyFileTokenID(String value) { _familyFileTokenID = value; } diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index e4ca4c25..b6c7fb38 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/services/security_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'; @@ -157,6 +158,10 @@ class AppDependencies { getIt.registerLazySingleton(() => PermissionService()); getIt.registerLazySingleton(() => TurnstileService(getIt())); + getIt.registerLazySingleton(() => SecurityServiceImpl( + appState: getIt(), + loggerService: getIt(), + )); // Repositories getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt())); diff --git a/lib/core/talsec_config.dart b/lib/core/talsec_config.dart new file mode 100644 index 00000000..1603d419 --- /dev/null +++ b/lib/core/talsec_config.dart @@ -0,0 +1,15 @@ +import 'package:freerasp/freerasp.dart'; + +final talsecConfig = TalsecConfig( + androidConfig: AndroidConfig( + packageName: 'com.cloudsolutions.HMGPatientApp', + signingCertHashes: ['6tvWaoN5coG4SnfxGbdQlcLmM0J4ePQwDjrKIg+QkV0=', 'j6VEqVhrypHMIiXiFdRLDdGwjGaMGWY7KAdBJA+Z4Pc='], // Must be the release cert hash + supportedStores: ['com.android.vending'], // Google Play Store + ), + iosConfig: IOSConfig( + bundleIds: ['com.cloudsolutions.HMGPatientApp'], // iOS Bundle ID + teamId: '3A359E86ZF', // Found in Apple Developer portal + ), + watcherMail: '', // Required to receive security alerts + isProd: true, // Enforces strict checks for release builds +); \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 87746f14..b22bbb6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,11 +53,15 @@ import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; 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/security_service.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; +import 'package:hmg_patient_app_new/unsafe_device.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' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; +import 'package:safe_device/safe_device.dart'; +import 'package:safe_device/safe_device_config.dart'; import 'core/utils/size_utils.dart'; import 'features/monthly_reports/terms_conditions_view_model.dart'; @@ -72,12 +76,12 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs -class MyHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; - } -} +// class MyHttpOverrides extends HttpOverrides { +// @override +// HttpClient createHttpClient(SecurityContext? context) { +// return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; +// } +// } Future callAppStateInitializations() async { final String deviceTypeId = (Platform.isIOS @@ -110,6 +114,10 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + SafeDevice.init( + SafeDeviceConfig(mockLocationCheckEnabled: false), // disables mock location check on Android + ); + try { // Attempt to get the default app. If it exists, this avoids the error. await Firebase.app(); @@ -122,9 +130,29 @@ Future callInitializations() async { await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - HttpOverrides.global = MyHttpOverrides(); + // HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); + // Initialize Security Service early to catch threats before app logic runs + if (kReleaseMode) { + await getIt.get().initialize(); + } + + // Set up critical threat callback to navigate to unsafe device page + getIt.get().setOnCriticalThreatCallback(() { + final navigationService = getIt.get(); + final context = navigationService.navigatorKey.currentContext; + + if (context != null) { + // Clear entire navigation stack and show unsafe device page + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const UnsafeDevice()), + (route) => false, // Remove all previous routes + ); + getIt.get().logError('🚨 Navigated to UnsafeDevice page - Critical threat detected'); + } + }); + // Initialize App Lifecycle Service to monitor background/foreground transitions getIt.get().initialize(); diff --git a/lib/services/security_service.dart b/lib/services/security_service.dart new file mode 100644 index 00000000..b72dffeb --- /dev/null +++ b/lib/services/security_service.dart @@ -0,0 +1,157 @@ +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/talsec_config.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Enum to categorize threat severity levels +enum ThreatSeverity { + critical, // Block app usage + warning, // Log only, don't block +} + +/// Model to track threat details +class ThreatEvent { + final String threatType; + final ThreatSeverity severity; + final DateTime timestamp; + final String? additionalInfo; + + ThreatEvent({ + required this.threatType, + required this.severity, + this.additionalInfo, + }) : timestamp = DateTime.now(); +} + +/// Callback type for critical threat detection +typedef OnCriticalThreatDetected = void Function(); + +/// Abstract class defining the security service interface +abstract class SecurityService { + /// Initialize and start the security monitoring + Future initialize(); + + /// Check if device is currently safe + bool get isSafeDevice; + + /// Get list of detected threats + List get detectedThreats; + + /// Set callback for when critical threat is detected + void setOnCriticalThreatCallback(OnCriticalThreatDetected callback); +} + +/// Implementation of SecurityService using Talsec (freeRASP) +class SecurityServiceImpl implements SecurityService { + final AppState appState; + final LoggerService loggerService; + + final List _detectedThreats = []; + bool _isInitialized = false; + OnCriticalThreatDetected? _onCriticalThreatCallback; + + SecurityServiceImpl({ + required this.appState, + required this.loggerService, + }); + + @override + bool get isSafeDevice => appState.isSafeDevice; + + @override + List get detectedThreats => List.unmodifiable(_detectedThreats); + + @override + Future initialize() async { + if (_isInitialized) { + loggerService.logInfo('SecurityService already initialized'); + return; + } + + try { + loggerService.logInfo('Initializing SecurityService with Talsec'); + + // Start the RASP engine + await Talsec.instance.start(talsecConfig); + + // Setup threat callbacks + final callback = ThreatCallback( + onAppIntegrity: () => _handleThreat('App Integrity', ThreatSeverity.critical), + onObfuscationIssues: () => _handleThreat('Obfuscation Issues', ThreatSeverity.warning), + onDebug: () => _handleThreat('Debug Mode', ThreatSeverity.critical), + onDeviceBinding: () => _handleThreat('Device Binding', ThreatSeverity.critical), + onDeviceID: () => _handleThreat('Device ID Mismatch', ThreatSeverity.critical), + onHooks: () => _handleThreat('Hooks Detected', ThreatSeverity.critical), + onPasscode: () => _handleThreat('Passcode Not Set', ThreatSeverity.warning), + onPrivilegedAccess: () => _handleThreat('Privileged Access (Root/Jailbreak)', ThreatSeverity.critical), + onSecureHardwareNotAvailable: () => _handleThreat('Secure Hardware Not Available', ThreatSeverity.warning), + onSimulator: () => _handleThreat('Simulator/Emulator Detected', ThreatSeverity.critical), + onSystemVPN: () => _handleThreat('System VPN Active', ThreatSeverity.warning), + onDevMode: () => _handleThreat('Developer Mode', ThreatSeverity.warning), + onADBEnabled: () => _handleThreat('USB Debugging Enabled', ThreatSeverity.warning), + onUnofficialStore: () => _handleThreat('Unofficial Store Installation', ThreatSeverity.critical), + onScreenshot: () => _handleThreat('Screenshot Detected', ThreatSeverity.warning), + onScreenRecording: () => _handleThreat('Screen Recording Active', ThreatSeverity.warning), + onMultiInstance: () => _handleThreat('Multiple Instances', ThreatSeverity.warning), + onLocationSpoofing: () => _handleThreat('Location Spoofing', ThreatSeverity.warning), + onTimeSpoofing: () => _handleThreat('Time Spoofing', ThreatSeverity.warning), + onAutomation: () => _handleThreat('Automation Detected', ThreatSeverity.warning), + onBootloader: () => _handleThreat('Unlocked Bootloader', ThreatSeverity.critical), + onMalware: (suspiciousApps) => _handleThreat('Malware/Suspicious Apps', ThreatSeverity.critical, additionalInfo: suspiciousApps.toString()), + ); + + Talsec.instance.attachListener(callback); + + _isInitialized = true; + loggerService.logInfo('SecurityService initialized successfully'); + } catch (e) { + loggerService.logError('Failed to initialize SecurityService: $e'); + if (!kDebugMode) { + FirebaseCrashlytics.instance.recordError( + e, + StackTrace.current, + reason: 'SecurityService initialization failed', + fatal: false, + ); + } + rethrow; + } + } + + @override + void setOnCriticalThreatCallback(OnCriticalThreatDetected callback) { + _onCriticalThreatCallback = callback; + loggerService.logInfo('Critical threat callback registered'); + } + + /// Handle detected threats with appropriate severity + void _handleThreat(String threatType, ThreatSeverity severity, {String? additionalInfo}) { + final threat = ThreatEvent( + threatType: threatType, + severity: severity, + additionalInfo: additionalInfo, + ); + + _detectedThreats.add(threat); + + // Log to console + if (severity == ThreatSeverity.critical) { + loggerService.logError('🔴 CRITICAL THREAT: $threatType ${additionalInfo != null ? "- $additionalInfo" : ""}'); + } else { + loggerService.logInfo('⚠️ WARNING: $threatType ${additionalInfo != null ? "- $additionalInfo" : ""}'); + } + + // Block app if critical threat + if (severity == ThreatSeverity.critical) { + appState.setIsSafeDevice = false; + loggerService.logError('Device marked as UNSAFE due to: $threatType'); + + // Trigger callback to navigate to unsafe device page + if (_onCriticalThreatCallback != null) { + _onCriticalThreatCallback!(); + } + } + } +} diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 7407ffec..8b39bf28 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -26,8 +26,10 @@ 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/zoom_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/unsafe_device.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; +import 'package:safe_device/safe_device.dart'; import 'core/cache_consts.dart'; import 'core/utils/push_notification_handler.dart'; @@ -42,8 +44,15 @@ class SplashPage extends StatefulWidget { class _SplashScreenState extends State { late AuthenticationViewModel authVm; + bool isJailBroken = false; + bool isRealDevice = true; + bool isDevelopmentModeEnable = false; + Future initializeStuff() async { listenerEvent(); + if (kReleaseMode) { + checkDeviceSafety(); + } Timer( Duration(milliseconds: 500), () async { @@ -51,34 +60,43 @@ class _SplashScreenState extends State { PushNotificationHandler().init(context); // Asyncronously }, ); + await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { - bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; - - // Initialize NotificationService using dependency injection - final notificationService = getIt.get(); - await notificationService.initialize(onNotificationClick: (payload) { - // Handle notification click here - }); + if (isJailBroken || !isRealDevice || !getIt.get().isSafeDevice) { + // Critical threat detected - navigate to unsafe device page + Navigator.of(getIt.get().navigatorKey.currentContext!).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const UnsafeDevice()), + (route) => false, // Remove all previous routes + ); + } else { + bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; + // Initialize NotificationService using dependency injection + final notificationService = getIt.get(); + await notificationService.initialize(onNotificationClick: (payload) { + // Handle notification click here + }); - ZoomService().initializeZoomSDK(); + ZoomService().initializeZoomSDK(); - if (!kDebugMode) { - _initializeClarity(); - } + if (!kDebugMode) { + _initializeClarity(); + } - if (isAppOpenedFromCall) { - navigateToTeleConsult(); - } else { - if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); + if (isAppOpenedFromCall) { + navigateToTeleConsult(); } else { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: OnboardingScreen())); + } else { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.of(getIt.get().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + } } } }); + // var zoom = ZoomVideoSdk(); // InitConfig initConfig = InitConfig( // domain: "zoom.us", @@ -154,6 +172,29 @@ class _SplashScreenState extends State { ); } + void checkDeviceSafety() { + try { + SafeDevice.isJailBroken.then((bool value) { + isJailBroken = value; + }); + SafeDevice.isJailBrokenCustom.then((bool value) { + isJailBroken = value; + }); + SafeDevice.isRealDevice.then((value) { + isRealDevice = value; + }); + + if (Platform.isAndroid) { + // isOnExternalStorage = await SafeDevice.isOnExternalStorage; + // SafeDevice.isDevelopmentModeEnable.then((value) { + // isDevelopmentModeEnable = value; + // }); + } + } catch (error) { + print(error); + } + } + Future listenerEvent() async { print('Call Canceled : ------->'); diff --git a/lib/unsafe_device.dart b/lib/unsafe_device.dart new file mode 100644 index 00000000..4712d70a --- /dev/null +++ b/lib/unsafe_device.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class UnsafeDevice extends StatefulWidget { + const UnsafeDevice({super.key}); + + @override + State createState() => _UnsafeDeviceState(); +} + +class _UnsafeDeviceState extends State { + @override + void initState() { + Talsec.instance.detachListener(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo + Utils.buildImgWithAssets(icon: AppAssets.hmgLogo, width: MediaQuery.of(context).size.width * 0.7, height: 90.h, fit: BoxFit.contain), + SizedBox(height: 32.h), + // Warning Icon + Icon( + Icons.security, + size: 80.h, + color: AppColors.primaryRedColor, + ), + SizedBox(height: 24.h), + // Title + Text( + 'Unsafe Device Detected', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.bold, + color: AppColors.primaryRedColor, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16.h), + + // Description + Text( + 'For your security, this app cannot run on devices with security vulnerabilities.', + style: TextStyle( + fontSize: 16.f, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24.h), + // if (getIt.get().detectedThreats.isNotEmpty) ...[ + // Text( + // 'Detected Issues:', + // style: TextStyle( + // fontSize: 14.f, + // fontWeight: FontWeight.bold, + // color: Colors.black87, + // ), + // ), + // SizedBox(height: 8.h), + // Container( + // padding: EdgeInsets.all(12.w), + // decoration: BoxDecoration( + // color: Colors.red.withOpacity(0.1), + // borderRadius: BorderRadius.circular(8.r), + // border: Border.all(color: Colors.red.withOpacity(0.3)), + // ), + // child: Column( + // children: getIt + // .get() + // .detectedThreats + // .where((t) => t.severity == ThreatSeverity.critical) + // .take(5) // Show max 5 threats + // .map((threat) => Padding( + // padding: EdgeInsets.symmetric(vertical: 4.h), + // child: Row( + // children: [ + // Icon(Icons.error_outline, size: 16.f, color: Colors.red), + // SizedBox(width: 8.w), + // Expanded( + // child: Text( + // threat.threatType, + // style: TextStyle(fontSize: 12.f), + // ), + // ), + // ], + // ), + // )) + // .toList(), + // ), + // ), + // SizedBox(height: 24.h), + // ], + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4c6b92b3..0c5d78dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,6 +111,8 @@ dependencies: screen_brightness: ^1.0.1 flutter_screenshot_blocker: ^1.0.4 cloudflare_turnstile: ^3.7.2 + freerasp: ^8.2.1 + safe_device: ^1.4.1 dev_dependencies: flutter_test: From 6521775c71ddfc98944a536389c7e81d6df33dd3 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 18 Aug 2026 17:40:19 +0300 Subject: [PATCH 36/73] Unsafe device page updates --- lib/core/api_consts.dart | 4 +- .../wallet_payment_confirm_page.dart | 2 +- lib/unsafe_device.dart | 86 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 865b7e75..7d3b97d4 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.8; + static final double appVersionID = 21.9; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 537a7dd8..ce260ca0 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -498,7 +498,7 @@ class _WalletPaymentConfirmPageState extends State { isEnglishOnly: true, ), AppCustomChipWidget( - labelText: "${habibWalletVM.selectedHospital!.name}", + labelText: "${habibWalletVM.selectedHospital != null ? habibWalletVM.selectedHospital!.name : "Habib Hospital"}", ), ], ).paddingSymmetrical(24.h, 0.h), diff --git a/lib/unsafe_device.dart b/lib/unsafe_device.dart index 4712d70a..8bfe761f 100644 --- a/lib/unsafe_device.dart +++ b/lib/unsafe_device.dart @@ -65,49 +65,49 @@ class _UnsafeDeviceState extends State { textAlign: TextAlign.center, ), SizedBox(height: 24.h), - // if (getIt.get().detectedThreats.isNotEmpty) ...[ - // Text( - // 'Detected Issues:', - // style: TextStyle( - // fontSize: 14.f, - // fontWeight: FontWeight.bold, - // color: Colors.black87, - // ), - // ), - // SizedBox(height: 8.h), - // Container( - // padding: EdgeInsets.all(12.w), - // decoration: BoxDecoration( - // color: Colors.red.withOpacity(0.1), - // borderRadius: BorderRadius.circular(8.r), - // border: Border.all(color: Colors.red.withOpacity(0.3)), - // ), - // child: Column( - // children: getIt - // .get() - // .detectedThreats - // .where((t) => t.severity == ThreatSeverity.critical) - // .take(5) // Show max 5 threats - // .map((threat) => Padding( - // padding: EdgeInsets.symmetric(vertical: 4.h), - // child: Row( - // children: [ - // Icon(Icons.error_outline, size: 16.f, color: Colors.red), - // SizedBox(width: 8.w), - // Expanded( - // child: Text( - // threat.threatType, - // style: TextStyle(fontSize: 12.f), - // ), - // ), - // ], - // ), - // )) - // .toList(), - // ), - // ), - // SizedBox(height: 24.h), - // ], + if (getIt.get().detectedThreats.isNotEmpty) ...[ + Text( + 'Detected Issues:', + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 8.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Column( + children: getIt + .get() + .detectedThreats + .where((t) => t.severity == ThreatSeverity.critical) + .take(5) // Show max 5 threats + .map((threat) => Padding( + padding: EdgeInsets.symmetric(vertical: 4.h), + child: Row( + children: [ + Icon(Icons.error_outline, size: 16.f, color: AppColors.primaryRedColor), + SizedBox(width: 8.w), + Expanded( + child: Text( + threat.threatType, + style: TextStyle(fontSize: 12.f), + ), + ), + ], + ), + )) + .toList(), + ), + ), + SizedBox(height: 24.h), + ], ], ), ), From 7a7ebdfed797453e32902e4dddc717337647b3ca Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 19 Aug 2026 12:12:04 +0300 Subject: [PATCH 37/73] unsafe device changes --- lib/unsafe_device.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/unsafe_device.dart b/lib/unsafe_device.dart index 8bfe761f..6dc34602 100644 --- a/lib/unsafe_device.dart +++ b/lib/unsafe_device.dart @@ -1,3 +1,4 @@ +import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:flutter/material.dart'; import 'package:freerasp/freerasp.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -17,6 +18,7 @@ class UnsafeDevice extends StatefulWidget { class _UnsafeDeviceState extends State { @override void initState() { + Clarity.setCurrentScreenName('Unsafe Device Page'); Talsec.instance.detachListener(); super.initState(); } From 3bf5ca98d50c1f8d1173c6decbf3f47a5b990732 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 23 Aug 2026 15:25:38 +0300 Subject: [PATCH 38/73] Livecare Terms & conditions, PayTabs Tamara fixes --- assets/langs/ar-SA.json | 83 ++++++- assets/langs/en-US.json | 83 ++++++- .../immediate_livecare_view_model.dart | 9 + lib/features/payfort/payfort_view_model.dart | 2 +- lib/features/paytabs/paytabs_view_model.dart | 50 +++- lib/generated/locale_keys.g.dart | 81 ++++++ .../appointment_payment_page.dart | 30 ++- .../book_appointment_page.dart | 1 + .../immediate_livecare_payment_details.dart | 50 +++- .../livecare/livecare_terms_conditions.dart | 230 ++++++++++++++++++ lib/presentation/home/landing_page.dart | 1 + .../ancillary_order_payment_page.dart | 107 ++++++++ .../ancillary_procedures_details_page.dart | 3 + lib/presentation/todo_section/todo_page.dart | 3 +- 14 files changed, 719 insertions(+), 14 deletions(-) create mode 100644 lib/presentation/book_appointment/livecare/livecare_terms_conditions.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index c025a030..911856ab 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1969,5 +1969,86 @@ "hide": "إخفاء", "viewPatientQR": "عرض رمز الاستجابة السريعة للمريض", "patientQR": "رمز الاستجابة السريعة للمريض", - "forText": "عن" + "forText": "عن", + "liveCareTermsHeading": "شروط خدمات الرعاية الصحية عن بُعد", + "liveCareTermsIntroHeading": "مقدمة", + "liveCareTermsConditions1": "تعتبر شركة مجموعة الدكتور سليمان الحبيب للخدمات الطبية ('المجموعة') واحدة من الشركات الرائدة في قطاع الرعاية الصحية في المملكة العربية السعودية والتي تسعى دائمًا لتحسين خدماتها في المنطقة، وكخطوة لنقل خدماتنا إلى مستوى آخر ولتسهيل عملية تلقي خدمات الرعاية الصحية، فقد اعتمدنا نهج تقديم بعض خدماتنا عن بُعد للمرضى. تنطبق هذه الشروط على خدمات الرعاية الصحية عن بُعد التي نقدمها، والتي تتيح إجراء الاستشارات بين الطبيب والمريض عبر الإنترنت، باستخدام تقنية الاتصالات الصوتية أو المرئية (يشار إليها غالباً باسم 'خدمات الرعاية الصحية عن بُعد').", + "liveCareTermsConditions2": "الغرض من خدمات الرعاية الصحية عن بعد هو تسهيل الاتصالات بين الممارسين الصحيين والمرضى لتقديم خدمات الرعاية الصحية من خلال وسائل الاتصال الإلكترونية المتقدمة، والتي تتيح للممارسين الصحيين تقييم وتشخيص وعلاج ومتابعة المرضى عن بُعد دون الحاجة إلى حضور المرضى شخصياً.", + "liveCareTermsHeading2": "الاتفاقية", + "liveCareTermsConditions3": "يتم تقديم خدمات الرعاية الصحية عن بُعد عبر تطبيق الهاتف المحمول أو الموقع الإلكتروني للمجموعة. تنطبق الشروط التالية على خدمات الرعاية الصحية عن بُعد: ", + "liveCareTermsConditions3a": "شروط استخدام الموقع الالكتروني - ", + "liveCareTermsConditions3b": "تفاقية مستخدم التطبيق - ", + "liveCareTermsConditions3c": "روط استخدام الخدمات الإلكترونية - ", + "liveCareTermsConditions3d": "شروط خدمات الرعاية الصحية عن بُعد هذه - ", + "liveCareTermsConditions4": "بالموافقة على الاستمرار في خدمات الرعاية الصحية عن بُعد وقبول هذه الشروط، فإنك تدخل في اتفاقية مع المجموعة تتضمن جميع الشروط المذكورة أعلاه (اتفاقية خدمات الرعاية الصحية عن بُعد).", + "liveCareTermsConditions5": "يتعين عليك أيضاً قراءة سياسة الخصوصية الخاصة بنا حيث تحتوي سياسة الخصوصية على تفاصيل حول كيفية استخدامنا لمعلوماتك الشخصية وحقوقك بشأنها. من خلال متابعة استخدام خدمات الرعاية الصحية عن بُعد، فإنك توافق على استخدامنا لمعلوماتك الشخصية وفقاً لسياسة الخصوصية الخاصة بنا وتوافق صراحةً على استخدامنا لمعلوماتك الشخصية لأداء خدمات الرعاية الصحية عن بُعد وأداء واجباتنا المهنية كمقدم لخدمات الرعاية الطبية.", + "liveCareTermsConditions6": "يتعين عليك أيضاً قراءة سياسة الخصوصية الخاصة بنا حيث تحتوي سياسة الخصوصية على تفاصيل حول كيفية استخدامنا لمعلوماتك الشخصية وحقوقك بشأنها. من خلال متابعة استخدام خدمات الرعاية الصحية عن بُعد، فإنك توافق على استخدامنا لمعلوماتك الشخصية وفقاً لسياسة الخصوصية الخاصة بنا وتوافق صراحةً على استخدامنا لمعلوماتك الشخصية لأداء خدمات الرعاية الصحية عن بُعد وأداء واجباتنا المهنية كمقدم لخدمات الرعاية الطبية.", + "liveCareTermsHeading3": "1.\tالتعاريف", + "liveCareTermsConditions7": "المجموعة تعني شركة مجموعة الدكتور سليمان الحبيب للخدمات الطبية والشركات التابعة لها.", + "liveCareTermsConditions8": "خدمات الرعاية الصحية عن بُعد تعني خدمة الرعاية الصحية عن بُعد التي تقدمها المجموعة، والتي تتيح الاستشارة والتشخيص والمتابعة عن بُعد بين المريض والطبيب، باستخدام التطبيق و/أو موقع الويب.", + "liveCareTermsConditions9": "خدماتنا تعني بعض الخدمات التي تقدمها المجموعة والتي تغطي استعراض السجلات الطبية للمرضى ونتائج الفحوصات الطبية، وتوفير التشخيصات الطبية بالإضافة إلى توفر خدمة الرعاية الصحية عن بعد بالصوت والفيديو بين ممارسي الرعاية الصحية والمرضى.", + "liveCareTermsConditions10": "التطبيق يعني تطبيق (Dr. ALHABIB) وهو عبارة عن منصة يتم من خلالها تقديم خدماتنا ويمكن للمستخدمين الولوج إليها.", + "liveCareTermsConditions11": "موقع الويب يعني موقع الويب الخاص بالمجموعة وهو عبارة عن منصة يتم من خلالها تقديم خدماتنا ويمكن للمستخدمين الدخول إليها.", + "liveCareTermsConditions12": "الشروط والأحكام تعني شروط خدمات الرعاية الصحية عن بُعد هذه.", + "liveCareTermsConditions13": "المحتوى يعني أي رسالة أو بيانات أو معلومات أو نص أو صوت أو صور أو فيديو أو رسومات أو رمز أو أي مادة أخرى واردة في التطبيق أو موقع الويب أو يتم نقلها عبر التطبيق أو موقع الويب.", + "liveCareTermsHeading4": "2.\tتعهدات المستخدم", + "liveCareTermsConditions14": "2-1\tمن خلال دخولك إلى خدمات الرعاية الصحية عن بُعد واستخدامها، فإنك توافق على هذه الشروط والأحكام ويتعين عليك ألا تحاول الدخول إلى خدماتنا إذا كنت لا توافق عليها.", + "liveCareTermsConditions15": "2-2\tأنت تضمن وتقر وتتعهد بأن جميع المعلومات التي تقدمها عن نفسك وعن أي شخص آخر صحيحة ودقيقة.", + "liveCareTermsConditions16": "2-3\tخدمات الرعاية الصحية عن بعد مخصصة للاستخدام من قبل المرضى الموجودين فعلياً في المملكة العربية السعودية (المنطقة المسموح بها) ولا ينبغي استخدامها من قبل المرضى خارج المنطقة المسموح بها. بالموافقة على هذه الشروط والأحكام والدخول إلى خدمات الرعاية الصحية عن بُعد عبر التطبيق أو موقع الويب، فإنك تقر وتضمن لنا أنك موجود فعلياً في إحدى المناطق المسموح بها.", + "liveCareTermsHeading5": "3.\tالقيود على خدمات الرعاية الصحية عن بعد والمخاطر المرتبطة بها", + "liveCareTermsConditions17": "3-1\tلا تستخدم التطبيق أو موقع الويب أو خدمات الرعاية الصحية عن بُعد للتواصل بشأن الحالات الطبية الطارئة أو علاجها. لا التطبيق ولا موقع الويب ولا خدمات الرعاية الصحية عن بُعد توفر الرعاية في الحالات الطبية الطارئة. إذا كانت هناك حالة طبية طارئة عاجلة أو كانت هناك حاجة إلى استجابة سريعة فيما يتعلق بصحتك، فانتقل إلى أقرب مركز/مستشفى طوارئ أو اتصل على الفور بأي رقم هاتف للطوارئ.", + "liveCareTermsConditions18": "3-2\tخدمات الرعاية الصحية عن بعد ليست مناسبة للحالات الطبية الطارئة وقد تكون هناك بعض العلاجات أو الأدوية التي لا يمكن تقديمها دون استشارة متابعة في حضور المريض شخصياً. إذا كنت تعتقد أن لديك حالة طبية خطيرة، فعليك طلب استشارة شخصية مع أخصائي طبي مرخص.", + "liveCareTermsConditions19": "3-3\tتهدف خدمات الرعاية الصحية عن بُعد إلى توفير مزايا تشمل راحة المريض. لذلك، فهو قد يكون الأنسب كبديل للاستشارة الشخصية للأمور الطبية الروتينية. ومع ذلك، هناك مخاطر محتملة مرتبطة باستخدام أي خدمة للرعاية الصحية عن بُعد، بما في ذلك خدمات الرعاية الصحية عن بُعد، وقد تشمل هذه المخاطر على سبيل المثال لا الحصر ما يلي:", + "liveCareTermsConditions19a": "(1) المعلومات المنقولة قد لا تكون كافية (مثل ضعف دقة الصور) للسماح باتخاذ القرار الطبي المناسب من قبل الممارسين الصحيين", + "liveCareTermsConditions19b": "(2) قد يقرر الممارسين الصحيين أن المعلومات المنقولة ليست ذات جودة كافية، مما يستلزم مقابلة المريض وجهاً لوجه، أو على الأقل إعادة جدولة استشارة طبية صوتية ومرئية في وقت لاحق", + "liveCareTermsConditions19c": "(3) يمكن أن يحدث تأخير في تقديم الرعاية الطبية بسبب أوجه القصور أو الأعطال في الأجهزة الإلكترونية", + "liveCareTermsConditions19d": "(4) يعتمد الممارس الصحي على المريض لتقديم جميع المعلومات ذات الصلة شفوياً وعبر الاستخدام المناسب لجهاز الكاميرا الخاص بالمريض. من الممكن ألا يتم تقديم المعلومات إلى الممارس أو قد لا تكون واضحة بقدر الوضوح الذي يتوفر للممارس الصحي أثناء إجراء الفحص البدني الشخصي", + "liveCareTermsConditions19e": "(5) قد تفشل البروتوكولات الأمنية، مما يتسبب في انتهاك خصوصية المعلومات الطبية الشخصية. على الرغم من أننا نتخذ تدابير معقولة لحماية أمن المعلومات المنقولة عبر التطبيق و/أو الموقع الإلكتروني، إلا أنه لا يمكن أبدًا ضمان أن تكون عمليات الإرسال هذه آمنة تماماً، والمعلومات المنقولة عبر شبكات الاتصالات العامة خارجة عن سيطرتنا ولا يمكننا تأمينها.", + "liveCareTermsConditions20": "3-4\tفي حال كنت تستخدم خدمات الرعاية الصحية عن بُعد ولكنك تواجه أي حالة أو مخاوف مستمرة، فإننا نوصيك بحجز استشارة شخصية.", + "liveCareTermsHeading6": "4.\tإخلاء المسؤولية عن الضمان", + "liveCareTermsConditions21": "4-1\tنحن لا نضمن ولا نقر أو نتعهد بأن خدمات الرعاية الصحية عن بُعد ستلبي احتياجاتك، أو أنها ستكون مستمرة دون انقطاع أو سيتم توفيرها في الوقت المناسب أو أنها آمنة أو خالية من الأخطاء أو خالية من الفيروسات. لن نتحمل أي مسؤولية تجاهك في حالة تعذر حدوث جلسة خدمات الرعاية الصحية عن بُعد أو تمت مقاطعتها أو التخلي عنها بسبب عدم توفر منصتنا عبر الإنترنت ومع ذلك، في حالة تعذر علينا إجراء جلسة خدمات الرعاية الصحية عن بُعد بالكامل بسبب تعطل منصتنا، فلن نتقاضى منك أي أجور عن جلسة الاستشارة البديلة.", + "liveCareTermsConditions22": "4-2\tتُستثنى جميع الضمانات الصريحة أو الضمنية بموجب النظام، باستثناء تلك التي لا يمكن استثناؤها بشكل قانوني.", + "liveCareTermsConditions23": "4-3\tلا يوجد في هذا القسم 4 أي نص يُقصد منه استبعاد أو تقييد مسؤوليتنا عن أي مطالبة تتعلق بالإهمال الطبي، إلى الحد الذي لا يسمح لنا فيه النظام المعمول به في المنطقة المسموح بها ذات الصلة بالقيام بذلك.", + "liveCareTermsHeading7": "5.\tالأهلية والتسجيل والاستخدام", + "liveCareTermsConditions24": "5-1\tمن أجل الدخول إلى خدمات الرعاية الصحية عن بُعد، يتعين عليك أن تفي بمتطلبات الحد الأدنى للسن وهو 18 عاماً (يتم احتسابها وفقاً للتقويم الميلادي) وأن تتمتع بالأهلية القانونية الكاملة.", + "liveCareTermsConditions25": "5-2\tيجب أن يحصل أي شخص يقل عمره عن 18 عاماً (يتم احتسابها وفقاً للتقويم الميلادي) يقوم بالدخول إلى خدمات الرعاية الصحية عن بُعد على إذن من أحد الوالدين أو الوصي للقيام بذلك.", + "liveCareTermsConditions26": "5-3\tأنت تضمن وتقر وتتعهد بأن لديك الحق القانوني والقدرة على الدخول إلى التطبيق و/أو موقع الويب وفقاً لهذه الشروط والأحكام و، عند الاقتضاء، إجراء الحجوزات والمدفوعات من خلال التطبيق أو موقع الويب.", + "liveCareTermsConditions27": "5-4\tتتم عملية التسجيل وفق ما ورد في شروط استخدام الخدمات الإلكترونية", + "liveCareTermsConditions28": "5-5\tخدمات الرعاية الصحية عن بُعد مخصصة فقط لاستخدامك الشخصي واستخدام أطفالك تحت إشرافك. يتعين عليك ألا تشارك بيانات تسجيل الدخول الخاصة بك (اسم المستخدم وكلمة المرور وما إلى ذلك) مع أي شخص آخر ويجب أن تتخذ خطوات معقولة للحفاظ على أمان هذه البيانات. يتم تقديم خدماتنا على أساس أن لدينا معلومات معينة حول المستخدم، والتي قد تكون مرتبطة أو تم الحصول عليها من تفاصيل حسابك (العمر والجنس والتاريخ الطبي وما إلى ذلك). نحن لا نتحمل أي مسؤولية من أي نوع تجاهك أو تجاه أي شخص آخر إذا تم تقديم خدماتنا عبر حساب مستخدم إلى شخص آخر غير المستخدم المسجل. هذا مهم بشكل خاص لخدمات الرعاية الصحية عن بعد، حيث سيحتاج الممارس الصحي إلى تقييم المعلومات التي يقدمها المريض أثناء الاستشارة في سياق الملف الطبي للمريض وتاريخه.", + "liveCareTermsConditions29": "5-6\tأنت مسؤول عن التأكد من أن لديك جهازاً متصلاً بالإنترنت وقادراً على إرسال واستقبال الفيديو والصوت. نحن لسنا مسؤولين عن أي انقطاع في جلسة خدمات الرعاية الصحية عن بعد بسبب ناجم عن جهازك أو اتصالك بالإنترنت. أنت مسؤول عن جميع التكاليف المرتبطة باتصالك بالإنترنت واستخدامك لها.", + "liveCareTermsConditions30": "5-7\tقد نقوم من وقت لآخر بإصدار تحديثات لتطبيقنا و/أو موقع الويب الخاص بنا. يجب عليك التأكد من أنك تقوم بتشغيل أحدث إصدار من التطبيق. قد يؤدي استخدام الإصدارات القديمة من التطبيق إلى حدوث أخطاء فنية أثناء استخدامك لخدمات الرعاية الصحية عن بُعد وقد يعرضك ذلك لمخاطر أمنية إضافية.", + "liveCareTermsHeading8": "6.\tخدمات الرعاية الصحية عن بُعد", + "liveCareTermsConditions31": "خدمات الرعاية الصحية عن بُعد الخاصة بنا سوف تكون لتخصصات طبية محددة أو لمواعيد المتابعة أو إعادة صرف الدواء. ", + "liveCareTermsHeading9": "7.\tالأجور والسداد", + "liveCareTermsConditions32": "خدمات الرعاية الصحية عن بُعد الخاصة بنا سوف تكون لتخصصات طبية محددة أو لمواعيد المتابعة أو إعادة صرف الدواء. ", + "liveCareTermsConditions33": "7-1\tستتم الإشارة إلى أجور استشارة خدمات الرعاية الصحية عن بُعد قبل أن تختار المتابعة. بمجرد المتابعة، فإنك توافق على سداد الأجور. لا تشمل تلك الأجور الأولية أي أجور لاحقة مثل أجور الوصفات الطبية والاختبارات المعملية واستشارات المتابعة وما إلى ذلك من أجور وسيتم إبلاغك بها بعد الاستشارة الأولية.", + "liveCareTermsConditions34": "7-2\tيتم تحديد شروط وطرق السداد المعمول بها في شروط استخدام الخدمات عبر الإنترنت الخاصة بنا.", + "liveCareTermsConditions35": "7-3\tقد يكون من الممكن فرض أجور خدمات الرعاية الصحية عن بُعد مباشرة على مزود التأمين الطبي، على الرغم من أننا لا نقدم أي تعهد أو ضمان بأن هذا سيكون ممكناً في أي وقت. تضمنت شروط استخدام الخدمات عبر الإنترنت الخاصة بنا مزيد من التفاصيل حول ذلك. إذا كان هذا التسهيل الخدمي متاحاً واخترت استخدامه، فستحتاج إلى تزويدنا بتفاصيل شركة التأمين الطبي وتغطية وثيقة التأمين، ما لم تكن قد قمت بذلك مسبقاً وربطتها بحسابك. أنت تفوضنا بموجب هذا بالتواصل مع أي شركة تأمين وفق التفاصيل التي تقدمها لنا في وقت الشراء أو المرتبطة بحسابك من أجل الحصول على الموافقة على تكلفة الخدمات المقدمة لك. أنت تقر بأنه قد يُطلب منا تقديم تفاصيل محددة عن الخدمات الطبية التي تطلبها من أجل القيام بذلك وتوافق صراحة على هذا الإفصاح.", + "liveCareTermsHeading10": "8.\tالاتصالات و/أو الاستفسارات", + "liveCareTermsConditions36": "في حال وجود أي أسئلة و/أو استفسارات بخصوص استخدام خدمات الرعاية الصحية عن بُعد، يرجى التواصل معنا على: EServices.HMG@drsulaimanalhabib.com أو الاتصال على الرقم: 9553 525 011", + "liveCareTermsHeading11": "9.\tحدود المسؤولية", + "liveCareTermsConditions37": "9-1\tمع مراعاة أي مسؤوليات ينص عليها النظام والتي لا يمكن استبعادها بموجبه، لا تتحمل المجموعة أو أي من مديرينا أو موظفينا أو وكلائنا أو مقاولينا المسؤولية تجاهك عن أي خسائر وأضرار ومسؤوليات ومطالبات ونفقات (بما في ذلك على سبيل المثال لا الحصر الأتعاب القانونية وتكاليف الدفاع أو التسوية) أيا كانت، سواء كانت مباشرة أو غير مباشرة أو تبعية، ناشئة عن أو متعلقة باستخدامك لخدمات الرعاية الصحية عن بُعد، أياً كان سبب نشوئها، سواء كان ذلك بموجب عقد أو المسؤولية التقصيرية (بما في ذلك الإهمال) أو النظام أو غير ذلك.", + "liveCareTermsConditions38": "9-2\tلن نكون مسؤولين تجاهك أو تجاه أي شخص آخر عن أي خسارة ناتجة عن سبب خارج عن سيطرتنا. يتضمن ذلك تعطل المعدات الإلكترونية أو الميكانيكية أو خطوط الاتصالات (بما في ذلك الهاتف والكابل والإنترنت)، أو الدخول غير المصرح به، أو أفعال أو إغفالات الأطراف الثالثة، أو الفيروسات، أو السرقة، أو أخطاء المشغل، أو الطقس القاسي أو غير العادي (بما في ذلك الفيضانات، أو الزلازل، أو أي من أفعال القضاء والقدر الأخرى)، أو الحرائق والحروب والتمرد والأعمال الإرهابية وأعمال شغب والنزاع العمالي أو المشاكل العمالية الأخرى أو الحوادث أو الطوارئ أو التأخير أو الحجز الزائد أو الإلغاء، أو الزوال الملكي أو أي إجراء حكومي.", + "liveCareTermsHeading12": "10.\tإنهاء الاستخدام", + "liveCareTermsConditions39": "10-1\tيجوز لنا التوقف عن توفير خدمات الرعاية الصحية عن بُعد في أي وقت ولأي سبب. لن نتحمل أي مسؤولية عن أي توقف، بما في ذلك عندما تكون خلال الاستشارات أو دورات العلاج باستخدام خدمات الرعاية الصحية عن بُعد. سنساعدك للتمكن من حجز الاستشارات الشخصية بدلاً من ذلك أو في نقل المعلومات ذات الصلة إلى مزود طبي بديل لتمكينك من مواصلة العلاج (مع مراعاة أنظمة حماية البيانات والسرية والممارسات الطبية والممارسات الطبية الجيدة السائدة).", + "liveCareTermsConditions40": "10-2\tيجوز لنا إنهاء استخدامك ودخولك إلى خدمات الرعاية الصحية عن بُعد لأي سبب أو بدون سبب، في أي وقت، سواء بإشعارك أو بدونه ودون تحمل أي مسؤولية تجاهك، ومع ذلك قد تنشأ هذه المسؤولية (على الرغم من أننا سنأخذ واجباتنا الطبية تجاهك بعين الاعتبار في حالة القيام بذلك).", + "liveCareTermsConditions41": "10-3\tستظل جميع القيود وجميع إخلاءات الطرف من المسؤولية والاستثناءات والقيود الخاصة بمسؤوليتنا سارية بعد أي إنهاء. عند الإنهاء، يتعين عليك عدم الدخول إلى أو استخدام التطبيق و/أو موقع الويب بشكل مباشر أو غير مباشر.", + "liveCareTermsHeading13": "11.\tالقانون الحاكم والاختصاص القضائي", + "liveCareTermsConditions42": "النظام المعمول به على التطبيق و/أو شروط وأحكام مستخدم موقع الويب والنزاعات الناشئة عنها أو المتعلقة بها هي أنظمة المملكة العربية السعودية. وأنت تخضع بشكل لا رجوع فيه للاختصاص القضائي الحصري لمحاكم المملكة العربية السعودية.", + "liveCareTermsHeading14": "12.\tالإقرارات", + "liveCareTermsConditions43": "12-1\tباستثناء ما هو منصوص عليه صراحةً في هذه الشروط والأحكام أو ما يسمح به أي نظام معمول به، فإنك توافق على ما يلي:", + "liveCareTermsConditions43a": "(1) أنك قد قرأت وفهمت هذه الشروط والأحكام والبنود الأخرى التي تشكل جزءاً من اتفاقيتك معنا", + "liveCareTermsConditions43b": "(2) أنك قد قرأت سياسة الخصوصية الخاصة بنا وتوافق بموجبه صراحة على استخدامنا لمعلوماتك الشخصية كما هو موضح فيها. بالإضافة إلى ذلك، أنت تدرك أن رقم الاتصال أو عنوان البريد الإلكتروني الذي زودتنا به عند التسجيل سيتم استخدامه للاتصالات من قبل المجموعة من خلال الرسائل القصيرة أو البريد الإلكتروني أو أي طريقة أخرى فيما يتعلق بخدمات الرعاية الصحية عن بُعد، بما في ذلك إشعارات المواعيد والخدمات الطبية الحالية.", + "liveCareTermsConditions43c": "(3) أنت تدرك أن لديك الحق في حجب أو سحب موافقتك بشأن استخدام التطبيق و/أو موقع الويب في سياق رعايتك في أي وقت، دون التأثير على حقك في تلقي الرعاية أو العلاج في المستقبل؛", + "liveCareTermsConditions43d": "(4) أنت تدرك أنك قد تتوقع الحصول على مزايا من استخدام التطبيق و/أو موقع الويب فيما يتعلق برعايتك الصحية، ولكن لا يمكن ضمان أي نتائج أو تأكيدها؛", + "liveCareTermsConditions43e": "(5) أنت تدرك أنه قد تتم مشاركة معلومات الرعاية الصحية الخاصة بك مع موظفين آخرين في المجموعة لأغراض جدولة المواعيد والفواتير؛", + "liveCareTermsConditions43f": "(6) أنت تدرك أن صور الفيديو والتسجيلات الصوتية الخاصة بك قد يتم التقاطها وتخزينها إلكترونياً. وأنت تدرك كذلك أنه يمكن استخدام هذه التسجيلات لاحقاً فيما يتعلق بالإجراءات النظامية إذا لزم الأمر ولكن لن يتم استخدامها لأي غرض بخلاف توفير خدمات الرعاية الصحية عن بُعد وخدمات المتابعة الطبية لك. أنت تقر وتوافق على استخدام هذه الصور والتسجيلات الصوتية من قبلنا للأغراض الموضحة؛", + "liveCareTermsConditions43g": "(7) أنك تدرك مخاطر الاتصال عبر البريد الإلكتروني والرسائل النصية، ولا سيما مخاطر الخصوصية. أنت تدرك أن المجموعة لا يمكنها ضمان أمان وسرية البريد الإلكتروني أو الاتصالات النصية. لن تكون المجموعة مسؤولة عن الرسائل التي لم يتم استلامها أو تسليمها بسبب عطل فني أو الكشف عن معلومات سرية ما لم يكن ذلك بسبب سوء سلوك متعمد.", + "liveCareTermsHeading15": "13.\tشروط وأحكام عامة", + "liveCareTermsConditions44": "13-1\tيجوز لنا في أي وقت تغيير هذه الشروط والأحكام لأسباب أمنية أو نظامية أو تنظيمية ، أو لتعكس التحديثات أو التغييرات التي تطرأ على خدمات أو وظائف خدمات الرعاية الصحية عن بُعد أو التطبيق، عن طريق نشر الشروط والأحكام المتنوعة على التطبيق. سنخطرك في التطبيق بأن هذه الشروط والأحكام قد تغيرت قبل استخدامك التالي لخدمات الرعاية الصحية عن بُعد وستتاح لك الفرصة للموافقة على الشروط المعدلة قبل استخدام الخدمة مرة أخرى.", + "liveCareTermsConditions45": "13-2\tيجوز لنا أداء أي من التزاماتنا، وممارسة أي من الحقوق الممنوحة لنا بموجب هذه الشروط والأحكام، من خلال طرف ثالث. يجوز لنا التنازل عن أي من حقوقنا والتزاماتنا أو جميعها بموجب هذه الشروط والأحكام إلى أي طرف ثالث.", + "liveCareTermsConditions46": "13-3\tإذا تبين أن أي بند أو جزء من أي بند من هذه الشروط والأحكام باطل أو غير قابل للإنفاذ أو غير صالح، فسيتم فصله عن هذه الشروط والأحكام، تاركاً باقي الشروط سارية المفعول بكامل الأثر والنفاذ، بشرط ألا يغير فصل ذلك البند من الطبيعة الأساسية لهذه الشروط والأحكام.", + "liveCareTermsHeading16": "14.\tالشكاوى", + "liveCareTermsConditions47": "خدمات الرعاية الصحية عن بُعد الخاصة بنا سوف تكون لتخصصات طبية محددة أو لمواعيد المتابعة أو إعادة صرف الدواء. ", + "liveCareTermsConditions48": "إذا كانت لديك أي شكاوى أو مخاوف بشأن التطبيق و/أو موقع الويب أو خدماتنا أو كيفية تعاملنا مع معلوماتك الشخصية، فيرجى التواصل معنا على: EServices.HMG@drsulaimanalhabib.com أو الاتصال على الرقم: 9553 525 011" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 1f006d2c..aaafb124 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1959,7 +1959,88 @@ "hide": "Hide", "viewPatientQR": "View Patient QR Code", "patientQR": "Patient QR Code", - "forText": "For" + "forText": "For", + "liveCareTermsHeading": "TELEHEALTH SERVICES TERMS OF USE", + "liveCareTermsIntroHeading": "INTRODUCTION", + "liveCareTermsConditions1": "Dr. Sulaiman Al Habib Medical Services Group Company (the “Group”) is one of the leaders in the healthcare sector in the Kingdom of Saudi Arabia with an aim to always improve its services in the region, and as a step to take our services to another level and to facilitate the process of receiving healthcare services we have adopted the approach of providing some of our services remotely to patients. These terms apply to our Telehealth Services, which enable consultations to occur between a doctor and a patient over the internet, using audio or and video conferencing technology (often referred to as “Telehealth Services”).", + "liveCareTermsConditions2": "The purpose of the Telehealth Services is to facilitate communication between healthcare practitioners and patients to provide healthcare services through advanced electronic communication means, which allows healthcare professionals to evaluate, diagnose, treat and follow-up with patients remotely without the need for patients to attend in person.).", + "liveCareTermsHeading2": "AGREEMENT", + "liveCareTermsConditions3": "Telehealth Services is delivered over the Group’s mobile application or and Website. The following terms apply to the Telehealth Services: ", + "liveCareTermsConditions3a": " - Website Terms of Use", + "liveCareTermsConditions3b": " - App User Agreement", + "liveCareTermsConditions3c": " - Online Services Terms of Use", + "liveCareTermsConditions3d": " - These Telehealth Services User Terms of Use", + "liveCareTermsConditions4": "By agreeing to proceed with the Telehealth Services and accepting these terms, you enter into an agreement with The Group incorporating all the above terms (Telehealth Services Agreement).", + "liveCareTermsConditions5": "Our Privacy Policy should also be read. It contains details about how we use your personal information and your rights. By proceeding with the Telehealth Services, you agree to our use of your personal information in accordance with our Privacy Policy and you expressly consent to our use of your personal information for the performance of the Telehealth Services and the discharge of our professional duties as a medical care provider.", + "liveCareTermsConditions6": "Our Privacy Policy should also be read. It contains details about how we use your personal information and your rights. By proceeding with the Telehealth Services, you agree to our use of your personal information in accordance with our Privacy Policy and you expressly consent to our use of your personal information for the performance of the Telehealth Services and the discharge of our professional duties as a medical care provider.", + "liveCareTermsHeading3": "1. DEFINITIONS", + "liveCareTermsConditions7": "Group means Dr. Sulaiman Al Habib Medical Services Group Company and its subsidiaries. ", + "liveCareTermsConditions8": "Telehealth Services means the Group’s telehealth service, enabling remote consultation, diagnosis and follow-up between the patient and doctor, using the Application and or the Website.", + "liveCareTermsConditions9": "Our Services means certain services provided by the Group which covers viewing patients’ medical records and medical test results, providing medical diagnostics in addition to the availability of voice and video Telehealth Service between healthcare practitioners and patients.", + "liveCareTermsConditions10": "Application means Dr. ALHABIB Application which is a platform where Our Services are provided through and accessible by users. ", + "liveCareTermsConditions11": "Website means the Group internet website which is a platform where Our Services are provided through and accessible by users. ", + "liveCareTermsConditions12": "Terms & Conditions means these Telehealth Services Terms of Use. ", + "liveCareTermsConditions13": "Content means any message, data, information, text, sound, photos, video, graphics, code or any other material contained on the Application and or the Website or transmitted via the Application and or the Website. ", + "liveCareTermsHeading4": "2. USER UNDERTAKINGS", + "liveCareTermsConditions14": "2.1 By accessing and using Telehealth Services, you agree to these Terms & Conditions, and you should not attempt to access Our Services if you do not agree. ", + "liveCareTermsConditions15": "2.2 You warrant, represent and undertake that all information you provide about yourself and about anyone else is true and accurate. ", + "liveCareTermsConditions16": "2.3 Telehealth Services is intended for use by patients physically located in the Kingdom Saudi Arabia (Permitted Territory) and should not be used by patients outside the Permitted Territory. By agreeing to these Terms & Conditions and accessing Telehealth Services via the Application and or the Website, you represent and warrant to us that you are physically located in one of the Permitted Territory. ", + "liveCareTermsHeading5": "3. LIMITATIONS OF TELEHEALTH SERVICES AND ASSOCIATED RISKS", + "liveCareTermsConditions17": "3.1 Do not use the Application and or the Website or the Telehealth Services to communicate or treat medical emergencies. Neither the Application nor the Website nor Telehealth Services are intended for the provision of care in an emergency. IF THERE IS AN URGENT MEDICAL EMERGENCY OR A RESPONSE IS NEEDED QUICKLY REGARDING YOUR HEALTH, GO TO A NEARBY EMERGENCY CENTER/HOSPITAL OR CALL IMMEDIATELY ANY EMERGENCY PHONE NUMBER. ", + "liveCareTermsConditions18": "3.2 Telehealth Services is not appropriate for emergency conditions and there may be some treatments or medicines which cannot be provided without follow-up physical consultation. If you believe you have a serious medical condition, you should seek an in-person consultation with a licensed medical professional.", + "liveCareTermsConditions19": "3.3 Telehealth Services is intended to provide benefits including patient convenience. As such, it is most suitable as an alternative to in-person consultation for routine medical matters. However, there are potential risks associated with the use of any telehealth service, including Telehealth Services, and these risks may include but not be limited to the following: ", + "liveCareTermsConditions19a": "(a) information transmitted may not be sufficient (e.g. poor resolution of images) to allow for appropriate medical decision making by healthcare practitioners.", + "liveCareTermsConditions19b": "(b) healthcare practitioners may determine that the transmitted information is of inadequate quality, thus necessitating a face-to-face meeting with the patient, or at least a rescheduled voice and video medical consult. ", + "liveCareTermsConditions19c": "(c) delays in medical care could occur due to deficiencies or failures of the electronic devices. ", + "liveCareTermsConditions19d": "(d) the practitioner is dependent on the patient to provide all relevant information orally and via the appropriate use of the patient’s own camera device. It is possible that information may not be provided to the practitioner or may not be apparent which would have been apparent to the practitioner during an in-person physical examination. ", + "liveCareTermsConditions19e": "(e) security protocols could fail, causing a breach of privacy of personal medical information. Whilst we take reasonable measures to protect the security of the information transmitted over the Application and or the Website, such transmissions can never be guaranteed to be completely secure and information transmitted over public telecommunications networks is outside our control and cannot be secured by us.", + "liveCareTermsConditions20": "3.4 If you use Telehealth Services but are experiencing any ongoing condition or concerns, we recommend you schedule an in-person consultation. ", + "liveCareTermsHeading6": "4. DISCLAIMER OF WARRANTY ", + "liveCareTermsConditions21": "4.1 We do not warrant, represent or undertake that Telehealth Services will meet your needs, or that it will be uninterrupted, timely, secure, error-free or free from viruses. We shall have no liability to you if a Telehealth Services session cannot occur or is interrupted or abandoned due to any unavailability of our online platform, however, where the Telehealth Services session cannot be delivered fully due to the downtime of our own platform, we will not charge you for a replacement consultation.", + "liveCareTermsConditions22": "4.2 All warranties expressed or implied by law, except those that cannot be lawfully excluded, are excluded. ", + "liveCareTermsConditions23": "4.3 Nothing in this section 4 is intended to exclude or limit our liability for any medical negligence claim, to the extent the applicable law in the relevant Permitted Territory does not permit us to do so.", + "liveCareTermsHeading7": "5. ELIGIBILITY, REGISTRATION AND USE", + "liveCareTermsConditions24": "5.1 To access Telehealth Services, you must satisfy the minimum age requirement of 18 years (measured according to the Gregorian calendar) and have full legal capacity. ", + "liveCareTermsConditions25": "5.2 Any person under the age of 18 years (measured according to the Gregorian calendar) accessing Telehealth Services must have permission from their parent or guardian to do so.", + "liveCareTermsConditions26": "5.3 You warrant, represent and undertake that you possess the legal right and ability to access the Application and or the Website in accordance with these Terms & Conditions and, where relevant, to make bookings and payments through the Application and or the Website.", + "liveCareTermsConditions27": "5.4 The registration process is as provided in the Online Services Terms of Use.", + "liveCareTermsConditions28": "5.5 Telehealth Services is intended only for your own personal use and the use of your children under your supervision. You must not share your user access details (log-in name, password etc.) with any other person and must take reasonable steps to maintain the security of such details. Our services are delivered on the basis that we have certain information about the user, which may be connected to or obtained from your account details (age, gender, medical history etc.). We accept no responsibility whatsoever to you or any other person if our services are delivered via a user account to a person other than the registered user. This is particularly important for telehealth services, where the practitioner will need to assess the information, the patient provides during the consultation in the context of the patient’s medical file and history.", + "liveCareTermsConditions29": "5.6 You are responsible for ensuring you have a device connected to the internet and capable of transmitting and receiving video and sound. We are not responsible for any failure of a Telehealth Services session caused by your device or your internet connection. You are responsible for all costs associated with your own internet connection and use.", + "liveCareTermsConditions30": "5.7 From time to time we may release updates to our Application and or the Website. You should ensure that you are running the latest version of the Application. Use of old versions of the Application may lead to technical errors in your use of the Telehealth Services and may expose you to additional security risks.", + "liveCareTermsHeading8": "6. TELEHEALTH SERVICES", + "liveCareTermsConditions31": "Our Telehealth Services will be for specific medical specialties or follow-up or medication refill appointments.", + "liveCareTermsHeading9": "7. CHARGES AND PAYMENT", + "liveCareTermsConditions32": "Our Telehealth Services will be for specific medical specialties or follow-up or medication refill appointments.", + "liveCareTermsConditions33": "7.1 The charges for a Telehealth Services consultation will be indicated before you choose to proceed. By proceeding, you agree to pay the charges. Any subsequent charges, such as prescription charges, lab tests, follow-up consultations etc. are excluded from the initial charge and will be communicated to you following the initial consultation. ", + "liveCareTermsConditions34": "7.2 The applicable payment terms and methods are set out in our Online Services Terms of Use. ", + "liveCareTermsConditions35": "7.3 It may be possible to charge Telehealth Services directly to a provider of medical insurance, although we give no undertaking or warranty that this shall be possible at any given time. Further details are set out in our Online Services Terms of Use. If such facility is made available and you choose to use it, you will need to provide us with details of your medical insurer and policy coverage, unless you have already done so and linked them to your account. You hereby authorise us to contact any such insurer with the details you provide at the time of purchase, or which are linked to your account to seek approval of the cost of your services. You acknowledge that we may be required to provide specific details of the medical services sought by you to do so and you expressly consent to such disclosure.", + "liveCareTermsHeading10": "8. COMMUNICATION AND/OR INQUIRIES", + "liveCareTermsConditions36": "In case there are any questions and/or inquiries regarding the use of Telehealth Services please contact: EServices.HMG@drsulaimanalhabib.com or call 011 525 9553", + "liveCareTermsHeading11": "9. LIMITATION OF LIABILITY ", + "liveCareTermsConditions37": "9.1 Subject to any responsibilities implied by law and which cannot be excluded, neither the Group nor any of our directors, employees, agents or contractors, are liable to you for any losses, damages, liabilities, claims and expenses (including without limitation but not limited to legal costs and defense or settlement costs) whatsoever, whether direct, indirect or consequential, arising out of or referable to your use of Telehealth Services , however caused, whether in contract, tort (including negligence), statute or otherwise. ", + "liveCareTermsConditions38": "9.2 We will not be liable to you or anyone else for any loss resulting from a cause over which we do not have control. This includes failure of electronic or mechanical equipment or communications lines (including telephone, cable and Internet), unauthorized access, acts or omissions of third parties, viruses, theft, operator errors, severe or extraordinary weather (including flood, earthquake, or other act of God), fire, war, insurrection, terrorist act, riot, labor dispute or other labor problems, accident, emergency, delay, overbooking, cancellation, Royal demise or action of government", + "liveCareTermsHeading12": "10. TERMINATION OF USE", + "liveCareTermsConditions39": "10.1 We may discontinue the provision of Telehealth Services at any time and for any reason. We shall have no liability for any discontinuation, including where you are part-way through a series of consultations or courses of treatment using Telehealth Services. We will assist in enabling you to book in-person consultations instead or in transferring relevant information to an alternative medical provider to enable you to continue treatment (subject to data protection, confidentiality and medical practice laws and to prevailing medical good practice).", + "liveCareTermsConditions40": "10.2 We may terminate your use and access to Telehealth Services for any or no reason, at any time, with or without notice to you and without incurring any liability to you, however that liability may arise (although we will consider our medical duties to you, if doing so). ", + "liveCareTermsConditions41": "10.3 All restrictions and all disclaimers and exclusions and limitations of our liability will survive any termination. Upon termination, you must not directly or indirectly access or use the Application and or the Website.", + "liveCareTermsHeading13": "11. GOVERNING LAW AND JURISDICTION ", + "liveCareTermsConditions42": "The law applicable to the Application and or the Website User Terms and Conditions and to disputes arising out of or in connection with them are the laws of The Kingdom of Saudi Arabia. You irrevocably submit to the exclusive jurisdiction of the courts of the Kingdom of Saudi Arabia. ", + "liveCareTermsHeading14": "12. ACKNOWLEDGEMENTS", + "liveCareTermsConditions43": "12.1 Except as expressly set out in these Terms & Conditions or as permitted by any applicable law, you agree on the following: ", + "liveCareTermsConditions43a": "(a) you have read and understand these Terms & Conditions and the other terms forming part of your Agreement with us; ", + "liveCareTermsConditions43b": "(b) you have read our Privacy Policy and hereby expressly consent to our use of your personal information as described. In addition, you understand that the contact number or email address that you have provided on registration will be used for communications by the Group through SMS, email or any other method in connection with the Telehealth Services, including for appointment notifications and current medical services;", + "liveCareTermsConditions43c": "(c) you understand that you have the right to withhold or withdraw your consent to the use of the Application and or the Website in the course of your care at any time, without affecting your right to future care or treatment; ", + "liveCareTermsConditions43d": "(d) you understand that you may expect to receive benefits from the use of Application and or the Website concerning your healthcare, but that no results can be guaranteed or assured; ", + "liveCareTermsConditions43e": "(e) you understand that your healthcare information may be shared with other employees in the Group for scheduling and billing purposes;", + "liveCareTermsConditions43f": "(f) you understand that video images and audio recordings of you may be captured and stored electronically. You understand that these recordings may be later used in connection with legal proceedings if necessary but will not otherwise be used for any purpose other than providing Telehealth Services and follow-up medical services to you. You acknowledge and consent to the use of these images and audio recordings by us for the purposes described; ", + "liveCareTermsConditions43g": "(g) you understand the risks of communicating by email and text messages, particularly the privacy risks. You understand that the Group cannot guarantee the security and confidentiality of email or text communication. The Group will not be responsible for messages that are not received or delivered due to technical failure, or for disclosure of confidential information unless caused by intentional misconduct.", + "liveCareTermsHeading15": "13. GENERAL TERMS AND CONDITIONS ", + "liveCareTermsConditions44": "13.1 We may at any time vary these Terms & Conditions for security, legal or regulatory reasons, or to reflect updates or changes to the services or functionality of Telehealth Services or the Application, by publishing the varied Terms & Conditions on the Application. We will notify you on the Application that these Terms & Conditions have changed before your next use of the Telehealth Services and you will have the opportunity to agree to the revised terms before using the service again. ", + "liveCareTermsConditions45": "13.2 We may perform any of our obligations, and exercise any of the rights granted to us under these Terms & Conditions, through a third party. We may assign any or all our rights and obligations under these Terms & Conditions to any third party.", + "liveCareTermsConditions46": "13.3 If any clause or part of any clause of these Terms & Conditions is found to be void, unenforceable or invalid, then it will be severed from these Terms & Conditions, leaving the remainder in full force and effect, provided that the severance has not altered the basic nature of these Terms & Conditions. ", + "liveCareTermsHeading16": "14. COMPLAINTS", + "liveCareTermsConditions47": "Our Telehealth Services will be for specific medical specialties or follow-up or medication refill appointments.", + "liveCareTermsConditions48": "If you have any complaints or concerns about the Application and or the Website, Our Services, or how we handle your personal information please contact us on: EServices.HMG@drsulaimanalhabib.com or call 011 525 9553" } diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index 84a79c02..e2e8d2cb 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -59,16 +59,25 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { bool isShowNotEligibleDialog = false; String notEligibleErrorMsg = ""; + bool isTermsAccepted = false; + initImmediateLiveCare() { _appState = getIt(); immediateLiveCareClinicsList = []; mainLiveCareClinicList = []; isImmediateLiveCareClinicsLoading = true; + isTermsAccepted = false; liveCareSelectedCallType = 0; // 1- Video, 2- Audio, 3- Phone immediateLiveCareSelectedClinic = GetLiveCareClinicListResponseModel(); liveCareImmediateAppointmentFeesList = LiveCareImmediateAppointmentFeesList(); } + + onTermsAccepted() { + isTermsAccepted = !isTermsAccepted; + notifyListeners(); + } + setLiveCareSelectedCallType(int value) { liveCareSelectedCallType = value; notifyListeners(); diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index 031c3182..c4f96857 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -418,7 +418,7 @@ class PayfortViewModel extends ChangeNotifier { payfortRequestInsertModel.sHAResponsePhase = payfortProjectDetailsRespModel!.shaResponse; payfortRequestInsertModel.signature = sdkTokenResponse!.signature; payfortRequestInsertModel.language = getIt.get().isArabic() ? "ar" : "en"; - payfortRequestInsertModel.integrationId = "1"; + payfortRequestInsertModel.integrationId = "1"; // Payfort integration ID payfortRequestInsertModel.isSchedule = isScheduleLiveCare; payfortRequestInsertModel.appointmentDate = appointmentDate; payfortRequestInsertModel.appointmentNo = appointmentNo; diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index 508753ef..b2563452 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -12,10 +12,12 @@ 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/common_models/tamara_request_model.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/payfort/payfort_view_model.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'; @@ -365,7 +367,53 @@ class PayTabsViewModel extends ChangeNotifier { } } - startAlternatePayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) { + startAlternatePayment( + {required String liveServiceID, + required String appointmentDate, + required String appointmentNo, + required String doctorID, + required String transactionID, + required String amount, + required String fileNumber, + required String serviceID, + required String projectID, + required bool isSchedule, + Function(PaytabsTransactionResponseModel)? onSuccess, + Function(String)? onError}) async { + LoaderBottomSheet.showLoader(); + + TamaraRequestModel tamaraRequestModel = TamaraRequestModel(); + + tamaraRequestModel.merchantReference = transactionID; + tamaraRequestModel.merchantIdentifier = "Tamara"; + tamaraRequestModel.clientRequestID = transactionID; + tamaraRequestModel.amount = num.parse(amount); + tamaraRequestModel.currency = "SR"; + tamaraRequestModel.language = appState.isArabic() ? 'AR' : 'EN'; + tamaraRequestModel.commandType = "PURCHASE"; + tamaraRequestModel.customerEmail = "CustID_$fileNumber@HMG.com"; + tamaraRequestModel.orderDescription = "Ancillary Order Payment"; + tamaraRequestModel.isInstallment = true; + tamaraRequestModel.projectID = num.parse(projectID); + tamaraRequestModel.accessCode = getIt.get().getAuthenticatedUser()!.mobileNumber!; + tamaraRequestModel.appointmentNo = appointmentNo; + tamaraRequestModel.customerName = "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}"; + ; + tamaraRequestModel.fileNumber = fileNumber; + tamaraRequestModel.patientOutSA = getIt.get().getAuthenticatedUser()!.outSa == 1 ? true : false; + tamaraRequestModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + tamaraRequestModel.latitude = appState.userLat.toString(); + tamaraRequestModel.longitude = appState.userLong.toString(); + tamaraRequestModel.serviceID = serviceID; + tamaraRequestModel.liveServiceID = liveServiceID; + tamaraRequestModel.doctorID = (doctorID != "") ? doctorID.toString() : ""; + tamaraRequestModel.appointmentDate = appointmentDate; + tamaraRequestModel.isSchedule = isSchedule; + + await getIt.get().tamaraRequestInsert(tamaraRequestModel: tamaraRequestModel); + + LoaderBottomSheet.hideLoader(); + bool _callbackFired = false; // prevent duplicate callback invocations FlutterPaytabsBridge.startAlternativePaymentMethod(paymentConfiguration, (event) { diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 536f3355..7dbcfd75 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1951,5 +1951,86 @@ abstract class LocaleKeys { static const viewPatientQR = 'viewPatientQR'; static const patientQR = 'patientQR'; static const forText = 'forText'; + static const liveCareTermsHeading = 'liveCareTermsHeading'; + static const liveCareTermsIntroHeading = 'liveCareTermsIntroHeading'; + static const liveCareTermsConditions1 = 'liveCareTermsConditions1'; + static const liveCareTermsConditions2 = 'liveCareTermsConditions2'; + static const liveCareTermsHeading2 = 'liveCareTermsHeading2'; + static const liveCareTermsConditions3 = 'liveCareTermsConditions3'; + static const liveCareTermsConditions3a = 'liveCareTermsConditions3a'; + static const liveCareTermsConditions3b = 'liveCareTermsConditions3b'; + static const liveCareTermsConditions3c = 'liveCareTermsConditions3c'; + static const liveCareTermsConditions3d = 'liveCareTermsConditions3d'; + static const liveCareTermsConditions4 = 'liveCareTermsConditions4'; + static const liveCareTermsConditions5 = 'liveCareTermsConditions5'; + static const liveCareTermsConditions6 = 'liveCareTermsConditions6'; + static const liveCareTermsHeading3 = 'liveCareTermsHeading3'; + static const liveCareTermsConditions7 = 'liveCareTermsConditions7'; + static const liveCareTermsConditions8 = 'liveCareTermsConditions8'; + static const liveCareTermsConditions9 = 'liveCareTermsConditions9'; + static const liveCareTermsConditions10 = 'liveCareTermsConditions10'; + static const liveCareTermsConditions11 = 'liveCareTermsConditions11'; + static const liveCareTermsConditions12 = 'liveCareTermsConditions12'; + static const liveCareTermsConditions13 = 'liveCareTermsConditions13'; + static const liveCareTermsHeading4 = 'liveCareTermsHeading4'; + static const liveCareTermsConditions14 = 'liveCareTermsConditions14'; + static const liveCareTermsConditions15 = 'liveCareTermsConditions15'; + static const liveCareTermsConditions16 = 'liveCareTermsConditions16'; + static const liveCareTermsHeading5 = 'liveCareTermsHeading5'; + static const liveCareTermsConditions17 = 'liveCareTermsConditions17'; + static const liveCareTermsConditions18 = 'liveCareTermsConditions18'; + static const liveCareTermsConditions19 = 'liveCareTermsConditions19'; + static const liveCareTermsConditions19a = 'liveCareTermsConditions19a'; + static const liveCareTermsConditions19b = 'liveCareTermsConditions19b'; + static const liveCareTermsConditions19c = 'liveCareTermsConditions19c'; + static const liveCareTermsConditions19d = 'liveCareTermsConditions19d'; + static const liveCareTermsConditions19e = 'liveCareTermsConditions19e'; + static const liveCareTermsConditions20 = 'liveCareTermsConditions20'; + static const liveCareTermsHeading6 = 'liveCareTermsHeading6'; + static const liveCareTermsConditions21 = 'liveCareTermsConditions21'; + static const liveCareTermsConditions22 = 'liveCareTermsConditions22'; + static const liveCareTermsConditions23 = 'liveCareTermsConditions23'; + static const liveCareTermsHeading7 = 'liveCareTermsHeading7'; + static const liveCareTermsConditions24 = 'liveCareTermsConditions24'; + static const liveCareTermsConditions25 = 'liveCareTermsConditions25'; + static const liveCareTermsConditions26 = 'liveCareTermsConditions26'; + static const liveCareTermsConditions27 = 'liveCareTermsConditions27'; + static const liveCareTermsConditions28 = 'liveCareTermsConditions28'; + static const liveCareTermsConditions29 = 'liveCareTermsConditions29'; + static const liveCareTermsConditions30 = 'liveCareTermsConditions30'; + static const liveCareTermsHeading8 = 'liveCareTermsHeading8'; + static const liveCareTermsConditions31 = 'liveCareTermsConditions31'; + static const liveCareTermsHeading9 = 'liveCareTermsHeading9'; + static const liveCareTermsConditions32 = 'liveCareTermsConditions32'; + static const liveCareTermsConditions33 = 'liveCareTermsConditions33'; + static const liveCareTermsConditions34 = 'liveCareTermsConditions34'; + static const liveCareTermsConditions35 = 'liveCareTermsConditions35'; + static const liveCareTermsHeading10 = 'liveCareTermsHeading10'; + static const liveCareTermsConditions36 = 'liveCareTermsConditions36'; + static const liveCareTermsHeading11 = 'liveCareTermsHeading11'; + static const liveCareTermsConditions37 = 'liveCareTermsConditions37'; + static const liveCareTermsConditions38 = 'liveCareTermsConditions38'; + static const liveCareTermsHeading12 = 'liveCareTermsHeading12'; + static const liveCareTermsConditions39 = 'liveCareTermsConditions39'; + static const liveCareTermsConditions40 = 'liveCareTermsConditions40'; + static const liveCareTermsConditions41 = 'liveCareTermsConditions41'; + static const liveCareTermsHeading13 = 'liveCareTermsHeading13'; + static const liveCareTermsConditions42 = 'liveCareTermsConditions42'; + static const liveCareTermsHeading14 = 'liveCareTermsHeading14'; + static const liveCareTermsConditions43 = 'liveCareTermsConditions43'; + static const liveCareTermsConditions43a = 'liveCareTermsConditions43a'; + static const liveCareTermsConditions43b = 'liveCareTermsConditions43b'; + static const liveCareTermsConditions43c = 'liveCareTermsConditions43c'; + static const liveCareTermsConditions43d = 'liveCareTermsConditions43d'; + static const liveCareTermsConditions43e = 'liveCareTermsConditions43e'; + static const liveCareTermsConditions43f = 'liveCareTermsConditions43f'; + static const liveCareTermsConditions43g = 'liveCareTermsConditions43g'; + static const liveCareTermsHeading15 = 'liveCareTermsHeading15'; + static const liveCareTermsConditions44 = 'liveCareTermsConditions44'; + static const liveCareTermsConditions45 = 'liveCareTermsConditions45'; + static const liveCareTermsConditions46 = 'liveCareTermsConditions46'; + static const liveCareTermsHeading16 = 'liveCareTermsHeading16'; + static const liveCareTermsConditions47 = 'liveCareTermsConditions47'; + static const liveCareTermsConditions48 = 'liveCareTermsConditions48'; } diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 57b2c71d..5c8bacf3 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -632,15 +632,33 @@ class _AppointmentPaymentPageState extends State { ).paddingSymmetrical(16.h, 16.h), ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "TAMARA"; + + transID = Utils.getAppointmentTransID( + widget.patientAppointmentHistoryResponseModel.projectID, + widget.patientAppointmentHistoryResponseModel.clinicID, + widget.patientAppointmentHistoryResponseModel.appointmentNo, + ); + if (appState.isPaytabsEnabled) { paytabsViewModel.setAlternatePaymentConfiguration( "Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString())); - paytabsViewModel.startAlternatePayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { - tamaraOrderID = transactionData.cartID!; - transID = transactionData.transactionReference!; - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); - await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); - await payfortViewModel.markAppointmentAsTamaraPaid( + paytabsViewModel.startAlternatePayment( + liveServiceID: "0", + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(), + transactionID: transID, + amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), + fileNumber: widget.patientAppointmentHistoryResponseModel.patientID, + serviceID: "2", + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + isSchedule: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + tamaraOrderID = transactionData.cartID!; + // transID = transactionData.transactionReference!; + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + await payfortViewModel.markAppointmentAsTamaraPaid( projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index c8fcdb36..53d4b1b1 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -611,6 +611,7 @@ class _BookAppointmentPageState extends State { ), ); } else { + immediateLiveCareViewModel.initImmediateLiveCare(); Navigator.of(context).push( CustomPageRoute( page: SelectImmediateLiveCareClinicPage(), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 57fd5e94..c15ebed4 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -15,6 +16,7 @@ import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_liveca import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/livecare_terms_conditions.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; @@ -286,7 +288,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 17.h), + SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -299,18 +301,59 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), + GestureDetector( + onTap: immediateLiveCareVM.onTermsAccepted, + child: Row( + children: [ + Selector( + selector: (_, viewModel) => viewModel.isTermsAccepted, + shouldRebuild: (previous, next) => previous != next, + builder: (context, isTermsAccepted, child) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: isTermsAccepted ? AppColors.primaryRedBorderColor : AppColors.greyColor, width: 2.h), + ), + child: isTermsAccepted ? Icon(Icons.check, size: 16.f, color: Colors.white) : null, + ); + }, + ), + SizedBox(width: 12.h), + Row( + children: [ + "${LocaleKeys.iAcceptThe.tr(context: context)} ".toText14(isBold: true), + GestureDetector( + onTap: () { + Navigator.push( + context, + CustomPageRoute( + page: LiveCareTermsConditions(), + ), + ); + }, + child: LocaleKeys.termsConditoins.tr(context: context).toText14(isBold: true, isUnderLine: true, color: AppColors.primaryRedColor)), + ], + ), + ], + ), + ).paddingSymmetrical(24.h, 0.h), (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") // (true) ? CustomButton( + isDisabled: !immediateLiveCareVM.isTermsAccepted, text: LocaleKeys.confirmLiveCare.tr(context: context), onPressed: () async { await askVideoCallPermission(context).then((val) async { if (val) { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.confirmingLiveCareRequest.tr(context: context)); - await immediateLiveCareVM.addNewCallRequestForImmediateLiveCare( - "${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}"); + await immediateLiveCareVM.addNewCallRequestForImmediateLiveCare("${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}"); await immediateLiveCareVM.getPatientLiveCareHistory(); LoaderBottomSheet.hideLoader(); if (immediateLiveCareVM.patientHasPendingLiveCareRequest) { @@ -367,6 +410,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { iconSize: 18.h, ).paddingSymmetrical(24.h, 24.h) : CustomButton( + isDisabled: !immediateLiveCareVM.isTermsAccepted, text: LocaleKeys.payNow.tr(context: context), onPressed: () async { await askVideoCallPermission(context).then((val) { diff --git a/lib/presentation/book_appointment/livecare/livecare_terms_conditions.dart b/lib/presentation/book_appointment/livecare/livecare_terms_conditions.dart new file mode 100644 index 00000000..522eb1bb --- /dev/null +++ b/lib/presentation/book_appointment/livecare/livecare_terms_conditions.dart @@ -0,0 +1,230 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +import '../../../generated/locale_keys.g.dart'; + +class LiveCareTermsConditions extends StatelessWidget { + const LiveCareTermsConditions({super.key}); + + @override + Widget build(BuildContext context) { + final isArabic = getIt.get().isArabic(); + + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.livecare.tr(context: context), + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeading(context, LocaleKeys.liveCareTermsHeading.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsIntroHeading.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions1.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions2.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading2.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions3.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions3a.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions3b.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions3c.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions3d.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions4.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions5.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions6.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading3.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions7.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions8.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions9.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions10.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions11.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions12.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions13.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading4.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions14.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions15.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions16.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading5.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions17.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions18.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19a.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19b.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19c.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19d.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions19e.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions20.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading6.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions21.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions22.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions23.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading7.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions24.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions25.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions26.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions27.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions28.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions29.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions30.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading8.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions31.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading9.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions32.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions33.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions34.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions35.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading10.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions36.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading11.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions37.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions38.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading12.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions39.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions40.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions41.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading13.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions42.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading14.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43a.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43b.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43c.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43d.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43e.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43f.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions43g.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading15.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions44.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions45.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions46.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildHeading(context, LocaleKeys.liveCareTermsHeading16.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions47.tr(context: context), isArabic), + SizedBox(height: 12.h), + _buildContent(context, LocaleKeys.liveCareTermsConditions48.tr(context: context), isArabic), + SizedBox(height: 24.h), + ], + ), + ), + ), + ) + ], + ), + ); + } + + /// Helper method to build heading text with consistent styling + Widget _buildHeading(BuildContext context, String text, bool isArabic) { + return Text( + text, + style: TextStyle( + fontSize: 20.f, + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', + fontWeight: FontWeight.w700, + color: AppColors.textColor, + letterSpacing: -0.5, + height: 1.4, + ), + ); + } + + /// Helper method to build content text with consistent styling + Widget _buildContent(BuildContext context, String text, bool isArabic) { + return Text( + text, + style: TextStyle( + fontSize: 16.f, + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', + color: AppColors.textColor, + letterSpacing: -0.3, + height: 1.5, + ), + ); + } +} diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index db300d98..bf1a7b62 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -1450,6 +1450,7 @@ class _LandingPageState extends State { appointmentNoVida: order.appointmentNo ?? 0, orderNo: order.orderNo ?? 0, projectID: order.projectID ?? 0, + clinicID: 0, projectName: order.projectName ?? "", setupID: order.setupID ?? "", ), diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index 80b79a88..36a2d7c5 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -14,6 +14,8 @@ 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/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart'; @@ -36,6 +38,7 @@ class AncillaryOrderPaymentPage extends StatefulWidget { final int appointmentNoVida; final int orderNo; final int projectID; + final int clinicID; final String setupID; final List selectedProcedures; final num totalAmount; @@ -46,6 +49,7 @@ class AncillaryOrderPaymentPage extends StatefulWidget { required this.appointmentNoVida, required this.orderNo, required this.projectID, + required this.clinicID, required this.setupID, required this.selectedProcedures, required this.totalAmount, @@ -60,16 +64,38 @@ class _AncillaryOrderPaymentPageState extends State { late AppState appState; late TodoSectionViewModel todoSectionViewModel; late PayTabsViewModel paytabsViewModel; + late MyAppointmentsViewModel myAppointmentsViewModel; + late HabibWalletViewModel habibWalletVM; MyInAppBrowser? browser; String selectedPaymentMethod = ""; String transID = ""; + bool isShowTamara = false; + String tamaraPaymentStatus = ""; + String tamaraOrderID = ""; + @override void initState() { scheduleMicrotask(() { payfortViewModel.initPayfortViewModel(); payfortViewModel.setIsApplePayConfigurationLoading(false); + + myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { + // if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) { + if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && + myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.minLimit?.amount != null && + myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.maxLimit?.amount != null) { + if (widget.totalAmount >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + widget.totalAmount <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + if (mounted) { + setState(() { + isShowTamara = true; + }); + } + } + } + }); }); super.initState(); } @@ -80,6 +106,8 @@ class _AncillaryOrderPaymentPageState extends State { todoSectionViewModel = Provider.of(context); payfortViewModel = Provider.of(context); paytabsViewModel = Provider.of(context, listen: false); + myAppointmentsViewModel = Provider.of(context); + habibWalletVM = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -364,6 +392,85 @@ class _AncillaryOrderPaymentPageState extends State { } } }), + SizedBox(height: 16.h), + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h).toShimmer2(isShow: false), + SizedBox(height: 16.h), + LocaleKeys.tamara.tr(context: context).toText16(isBold: true).toShimmer2(isShow: false), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ).toShimmer2(isShow: false), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + transID = Utils.getAppointmentTransID( + widget.projectID, + widget.clinicID, + widget.appointmentNoVida, + ); + if (appState.isPaytabsEnabled) { + paytabsViewModel.setAlternatePaymentConfiguration("Ancillary Order Payment", widget.totalAmount); + paytabsViewModel.startAlternatePayment( + liveServiceID: "0", + appointmentDate: DateUtil.convertDateToString(widget.appointmentDate!), + appointmentNo: widget.appointmentNoVida.toString(), + doctorID: "0", + transactionID: transID, + amount: widget.totalAmount.toString(), + fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), + serviceID: "2", + projectID: widget.projectID.toString(), + isSchedule: false, + onSuccess: (PaytabsTransactionResponseModel transactionData) async { + tamaraOrderID = transactionData.cartID!; + // transID = transactionData.transactionReference!; + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + _createAdvancePayment( + paymentAmount: widget.totalAmount, + paymentReference: transactionData.transactionReference!, + paymentMethod: selectedPaymentMethod, + ); + }, + onError: (err) async { + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + _openPaymentURL("tamara"); + } + }) + : SizedBox.shrink(), ], ), ), diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index c34031f4..24a6d97b 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -29,6 +29,7 @@ class AncillaryOrderDetailsList extends StatefulWidget { final int appointmentNoVida; final int orderNo; final int projectID; + final int clinicID; final String projectName; final String setupID; @@ -37,6 +38,7 @@ class AncillaryOrderDetailsList extends StatefulWidget { required this.appointmentNoVida, required this.orderNo, required this.projectID, + required this.clinicID, required this.projectName, required this.setupID, }); @@ -682,6 +684,7 @@ class _AncillaryOrderDetailsListState extends State { appointmentNoVida: widget.appointmentNoVida, orderNo: widget.orderNo, projectID: widget.projectID, + clinicID: widget.clinicID, selectedProcedures: selectedProcedures, totalAmount: _getTotalAmount(), appointmentDate: orderData.appointmentDate, diff --git a/lib/presentation/todo_section/todo_page.dart b/lib/presentation/todo_section/todo_page.dart index d669668a..2f1061d2 100644 --- a/lib/presentation/todo_section/todo_page.dart +++ b/lib/presentation/todo_section/todo_page.dart @@ -83,7 +83,8 @@ class _ToDoPageState extends State { appointmentNoVida: order.appointmentNo ?? 0, orderNo: order.orderNo ?? 0, projectID: order.projectID ?? 0, - projectName: order.projectName ?? "", + clinicID: order.clinicID ?? 0, + projectName: order.projectName ?? "", setupID: order.setupID ?? "0", ), ), From 343b5655f8fa404a8313c14cc34e998d42c1c6b6 Mon Sep 17 00:00:00 2001 From: taha Date: Sun, 23 Aug 2026 15:46:43 +0300 Subject: [PATCH 39/73] refactor: streamline calculator result handling and UI updates(Aamir changes) --- .../book_appointment/select_clinic_page.dart | 788 ++++++++---------- .../health_calculator_detailed_page.dart | 197 +++-- 2 files changed, 458 insertions(+), 527 deletions(-) diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 785ebb62..5f76ab31 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -89,13 +89,15 @@ class _SelectClinicPageState extends State { child: CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () { - if (widget.calculatedResult["clinicId"] == null) { - bookAppointmentsViewModel.setIsDoctorsListLoading(true); - bookAppointmentsViewModel.isGetDocForHealthCal = true; - bookAppointmentsViewModel.calculationID = widget.calculatedResult["calculationID"]; - Navigator.push(context, CustomPageRoute(page: SelectDoctorPage())); - } else { - onClinicSelected(GetClinicsListResponseModel(clinicID: widget.calculatedResult["clinicId"], clinicDescription: "asdfds", clinicDescriptionN: "asdfds")); + if (widget.calculatedResult != null && widget.calculatedResult is Map) { + if (widget.calculatedResult["clinicId"] == null) { + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + bookAppointmentsViewModel.isGetDocForHealthCal = true; + bookAppointmentsViewModel.calculationID = widget.calculatedResult["calculationID"]; + Navigator.push(context, CustomPageRoute(page: SelectDoctorPage())); + } else { + onClinicSelected(GetClinicsListResponseModel(clinicID: widget.calculatedResult["clinicId"], clinicDescription: "asdfds", clinicDescriptionN: "asdfds")); + } } }, icon: null, @@ -132,13 +134,13 @@ class _SelectClinicPageState extends State { focusNode: textFocusNode, suffix: searchEditingController.text.isNotEmpty ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) : null, onChange: (value) { bookAppointmentsViewModel.filterClinics(value!); @@ -150,90 +152,90 @@ class _SelectClinicPageState extends State { ), bookAppointmentsVM.isLiveCareSchedule ? ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading - ? 5 - : bookAppointmentsVM.liveCareClinicsList.isNotEmpty - ? bookAppointmentsVM.liveCareClinicsList.length - : 1, + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading + ? 5 + : bookAppointmentsVM.liveCareClinicsList.isNotEmpty + ? bookAppointmentsVM.liveCareClinicsList.length + : 1, - // bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, + // bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: bookAppointmentsVM.liveCareClinicsList.isNotEmpty - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); - }) - : Utils.getNoDataWidget(context), - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ) + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: bookAppointmentsVM.liveCareClinicsList.isNotEmpty + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); + }) + : Utils.getNoDataWidget(context), + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ) : ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); - }), - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); + }), + ), + ), ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), ], ); }), @@ -272,14 +274,17 @@ class _SelectClinicPageState extends State { } Widget resultWidget({required HealthCalculatorsTypeEnum type, dynamic calculatedResult}) { + if (calculatedResult == null || calculatedResult is! Map) { + return const SizedBox.shrink(); + } AppState appState = getIt(); return Column( children: [ if (type == HealthCalculatorsTypeEnum.bmi) ...[ if (calculatedResult["bmiCategory"] == "Normal") ...[ calorieWidget( - title: calculatedResult["bmiCategory"], - calories: calculatedResult["bmiResult"], + title: calculatedResult["bmiCategory"]?.toString() ?? "", + calories: calculatedResult["bmiResult"] ?? 0, description: LocaleKeys.bmiNormalDescription.tr(), color: Colors.green, icon: Icons.scale, @@ -288,8 +293,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["bmiCategory"] == "Underweight") ...[ calorieWidget( - title: calculatedResult["bmiCategory"], - calories: calculatedResult["bmiResult"], + title: calculatedResult["bmiCategory"]?.toString() ?? "", + calories: calculatedResult["bmiResult"] ?? 0, description: LocaleKeys.bmiUnderweightDescription.tr(), color: Colors.orange, hide: true, @@ -298,8 +303,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["bmiCategory"] == "Overweight") ...[ calorieWidget( - title: calculatedResult["bmiCategory"], - calories: calculatedResult["bmiResult"], + title: calculatedResult["bmiCategory"]?.toString() ?? "", + calories: calculatedResult["bmiResult"] ?? 0, description: LocaleKeys.bmiOverweightDescription.tr(), color: Colors.red, hide: true, @@ -309,8 +314,8 @@ class _SelectClinicPageState extends State { ] else if (type == HealthCalculatorsTypeEnum.calories) ...[ if (calculatedResult["calorieRange"] == "Normal") ...[ calorieWidget( - title: calculatedResult["calorieRange"], - calories: calculatedResult["calories"], + title: calculatedResult["calorieRange"]?.toString() ?? "", + calories: calculatedResult["calories"] ?? 0, description: LocaleKeys.caloriesMaintenanceDescription.tr(), color: Colors.green, icon: Icons.scale, @@ -318,8 +323,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["calorieRange"] == "Low") ...[ calorieWidget( - title: calculatedResult["calorieRange"], - calories: calculatedResult["calories"], + title: calculatedResult["calorieRange"]?.toString() ?? "", + calories: calculatedResult["calories"] ?? 0, description: LocaleKeys.caloriesModerateDeficitDescription.tr(), color: Colors.orange, icon: Icons.trending_down, @@ -327,8 +332,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["calorieRange"] == "High") ...[ calorieWidget( - title: calculatedResult["calorieRange"], - calories: calculatedResult["calories"], + title: calculatedResult["calorieRange"]?.toString() ?? "", + calories: calculatedResult["calories"] ?? 0, description: LocaleKeys.caloriesAggressiveDeficitDescription.tr(), color: Colors.red, icon: Icons.warning_rounded, @@ -337,8 +342,8 @@ class _SelectClinicPageState extends State { ] else if (type == HealthCalculatorsTypeEnum.bodyFat) ...[ if (calculatedResult["fatCategory"] == "Essential Fat" || calculatedResult["fatCategory"] == "Athletes") ...[ calorieWidget( - title: calculatedResult["fatCategory"], - calories: calculatedResult["fatPercentage"], + title: calculatedResult["fatCategory"]?.toString() ?? "", + calories: calculatedResult["fatPercentage"] ?? 0, description: LocaleKeys.bodyFatEssentialDescription.tr(), color: Colors.green, hide: true, @@ -347,8 +352,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["fatCategory"] == "Fitness") ...[ calorieWidget( - title: calculatedResult["fatCategory"], - calories: calculatedResult["fatPercentage"], + title: calculatedResult["fatCategory"]?.toString() ?? "", + calories: calculatedResult["fatPercentage"] ?? 0, description: LocaleKeys.bodyFatFitnessDescription.tr(), color: Colors.green, hide: true, @@ -357,8 +362,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["fatCategory"] == "Average" || calculatedResult["fatCategory"] == "Acceptable") ...[ calorieWidget( - title: calculatedResult["fatCategory"], - calories: calculatedResult["fatPercentage"], + title: calculatedResult["fatCategory"]?.toString() ?? "", + calories: calculatedResult["fatPercentage"] ?? 0, description: LocaleKeys.bodyFatAverageDescription.tr(), color: Colors.orange, hide: true, @@ -367,8 +372,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["fatCategory"] == "Obese" || calculatedResult["fatCategory"] == "Overweight") ...[ calorieWidget( - title: calculatedResult["fatCategory"], - calories: calculatedResult["fatPercentage"], + title: calculatedResult["fatCategory"]?.toString() ?? "", + calories: calculatedResult["fatPercentage"] ?? 0, description: LocaleKeys.bodyFatObeseDescription.tr(), color: Colors.red, hide: true, @@ -379,8 +384,8 @@ class _SelectClinicPageState extends State { ] else if (type == HealthCalculatorsTypeEnum.bmr) ...[ if (calculatedResult["bmrRange"] == "Normal") ...[ calorieWidget( - title: calculatedResult["bmrRange"], - calories: calculatedResult["bmr"], + title: calculatedResult["bmrRange"]?.toString() ?? "", + calories: calculatedResult["bmr"] ?? 0, description: LocaleKeys.bmrNormalDescription.tr(), color: Colors.green, hide: true, @@ -389,8 +394,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["bmrRange"] == "Low") ...[ calorieWidget( - title: calculatedResult["bmrRange"], - calories: calculatedResult["bmr"], + title: calculatedResult["bmrRange"]?.toString() ?? "", + calories: calculatedResult["bmr"] ?? 0, description: LocaleKeys.low.tr(), color: Colors.orange, hide: true, @@ -399,8 +404,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["bmrRange"] == "High") ...[ calorieWidget( - title: calculatedResult["bmrRange"], - calories: calculatedResult["bmr"], + title: calculatedResult["bmrRange"]?.toString() ?? "", + calories: calculatedResult["bmr"] ?? 0, description: LocaleKeys.bmrHighDescription.tr(), color: Colors.red, hide: true, @@ -410,8 +415,8 @@ class _SelectClinicPageState extends State { ] else if (type == HealthCalculatorsTypeEnum.idealBodyWeight) ...[ if (calculatedResult["status"] == "Normal") ...[ calorieWidget( - title: calculatedResult["status"], - calories: calculatedResult["ibw"], + title: calculatedResult["status"]?.toString() ?? "", + calories: calculatedResult["ibw"] ?? 0, hide: true, description: (() { final diff = calculatedResult["difference"]; @@ -432,8 +437,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["status"] == "Underweight") ...[ calorieWidget( - title: calculatedResult["status"], - calories: calculatedResult["ibw"], + title: calculatedResult["status"]?.toString() ?? "", + calories: calculatedResult["ibw"] ?? 0, hide: true, description: (() { final diff = calculatedResult["difference"]; @@ -448,8 +453,8 @@ class _SelectClinicPageState extends State { SizedBox(height: 16.h), ] else if (calculatedResult["status"] == "Overweight") ...[ calorieWidget( - title: calculatedResult["status"], - calories: calculatedResult["ibw"], + title: calculatedResult["status"]?.toString() ?? "", + calories: calculatedResult["ibw"] ?? 0, hide: true, description: (() { final diff = calculatedResult["difference"]; @@ -463,151 +468,41 @@ class _SelectClinicPageState extends State { ), ] ] else if (type == HealthCalculatorsTypeEnum.crabsProteinFat) ...[ - if (calculatedResult["dietType"] == "Very Low Crabs") ...[ - calorieWidget( - title: calculatedResult["dietType"].toString(), - calories: calculatedResult["totalCalories"], - description: (() { - final total = calculatedResult["totalCalories"]; - final carbsG = calculatedResult["carbsGrams"]; - final carbsC = calculatedResult["carbsCalories"]; - final protG = calculatedResult["proteinGrams"]; - final protC = calculatedResult["proteinCalories"]; - final fatG = calculatedResult["fatGrams"]; - final fatC = calculatedResult["fatCalories"]; - final base = - 'Very low‑carb (ketogenic) approach: minimal carbohydrates, higher fat and moderate protein. May support rapid weight loss and improved blood sugar control for some, but can be hard to sustain and may cause nutrient gaps. Monitor hydration/electrolytes and consult a professional for long‑term use.'; - // if (total is num) { - // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown: Carbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; - // } - return base; - })(), - chipsItems: [ - ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), - ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), - ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), - ], - color: Colors.orange, - hide: true, - icon: Icons.trending_down, - ), - SizedBox(height: 16.h), - ] else if (calculatedResult["dietType"] == "Low Crabs") ...[ - calorieWidget( - title: calculatedResult["dietType"].toString(), - calories: calculatedResult["totalCalories"], - description: (() { - final total = calculatedResult["totalCalories"]; - final carbsG = calculatedResult["carbsGrams"]; - final carbsC = calculatedResult["carbsCalories"]; - final protG = calculatedResult["proteinGrams"]; - final protC = calculatedResult["proteinCalories"]; - final fatG = calculatedResult["fatGrams"]; - final fatC = calculatedResult["fatCalories"]; - final base = - 'Low‑carb, higher‑protein plan: reduced carbohydrates with increased protein to support satiety and muscle maintenance. Helpful for weight management and glycemic control when balanced with vegetables and healthy fats. Ensure adequate fiber and micronutrients.'; - // if (total is num) { - // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; - // } - return base; - })(), - chipsItems: [ - ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), - ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), - ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), - ], - color: Colors.orange, - hide: true, - icon: Icons.trending_down, - ), - SizedBox(height: 16.h), - ] else if (calculatedResult["dietType"] == "Moderate Crabs") ...[ + if (calculatedResult["dietType"] == "Very Low Crabs" || + calculatedResult["dietType"] == "Low Crabs" || + calculatedResult["dietType"] == "Moderate Crabs" || + calculatedResult["dietType"] == "USDA Guidelines " || + calculatedResult["dietType"] == "Zone Diet") ...[ calorieWidget( - title: calculatedResult["dietType"].toString(), - calories: calculatedResult["totalCalories"], + title: calculatedResult["dietType"]?.toString() ?? "", + calories: calculatedResult["totalCalories"] ?? 0, description: (() { - final total = calculatedResult["totalCalories"]; - final carbsG = calculatedResult["carbsGrams"]; - final carbsC = calculatedResult["carbsCalories"]; - final protG = calculatedResult["proteinGrams"]; - final protC = calculatedResult["proteinCalories"]; - final fatG = calculatedResult["fatGrams"]; - final fatC = calculatedResult["fatCalories"]; - final base = - 'Moderate carbohydrate intake with balanced protein and fats: provides steady energy for daily activity and exercise while allowing flexibility. Suitable for many people aiming for sustainable weight management and performance.'; - // if (total is num) { - // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; - // } - return base; - })(), - chipsItems: [ - ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), - ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), - ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), - ], - color: Colors.green, - hide: true, - icon: Icons.scale, - ), - SizedBox(height: 16.h), - ] else if (calculatedResult["dietType"] == "USDA Guidelines ") ...[ - calorieWidget( - title: calculatedResult["dietType"].toString(), - calories: calculatedResult["totalCalories"], - description: (() { - final total = calculatedResult["totalCalories"]; - final carbsG = calculatedResult["carbsGrams"]; - final carbsC = calculatedResult["carbsCalories"]; - final protG = calculatedResult["proteinGrams"]; - final protC = calculatedResult["proteinCalories"]; - final fatG = calculatedResult["fatGrams"]; - final fatC = calculatedResult["fatCalories"]; - final base = - 'USDA‑based balanced pattern: emphasizes fruits, vegetables, whole grains, lean proteins, and limited added sugars and saturated fats. Evidence‑based framework for general health, nutrient adequacy, and chronic disease prevention.'; - // if (total is num) { - // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; - // } - - return base; + final dt = calculatedResult["dietType"]; + if (dt == "Very Low Crabs") { + return 'Very low‑carb (ketogenic) approach: minimal carbohydrates, higher fat and moderate protein. May support rapid weight loss and improved blood sugar control for some, but can be hard to sustain and may cause nutrient gaps. Monitor hydration/electrolytes and consult a professional for long‑term use.'; + } else if (dt == "Low Crabs") { + return 'Low‑carb, higher‑protein plan: reduced carbohydrates with increased protein to support satiety and muscle maintenance. Helpful for weight management and glycemic control when balanced with vegetables and healthy fats. Ensure adequate fiber and micronutrients.'; + } else if (dt == "Moderate Crabs") { + return 'Moderate carbohydrate intake with balanced protein and fats: provides steady energy for daily activity and exercise while allowing flexibility. Suitable for many people aiming for sustainable weight management and performance.'; + } else if (dt == "USDA Guidelines ") { + return 'USDA‑based balanced pattern: emphasizes fruits, vegetables, whole grains, lean proteins, and limited added sugars and saturated fats. Evidence‑based framework for general health, nutrient adequacy, and chronic disease prevention.'; + } else { + return 'Zone Diet (~40% carbs / 30% protein / 30% fat): emphasizes hormonal balance and portion control. May improve body composition and energy for some, but requires planning; personalize with a nutrition professional.'; + } })(), chipsItems: [ - ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), - ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), - ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + if (calculatedResult["carbsGrams"] != null && calculatedResult["carbsCalories"] != null) + ('Carbohydrates: ' + ((calculatedResult["carbsGrams"] is num) ? (calculatedResult["carbsGrams"] as num).toStringAsFixed(0) : calculatedResult["carbsGrams"].toString()) + 'g = ' + ((calculatedResult["carbsCalories"] is num) ? (calculatedResult["carbsCalories"] as num).toStringAsFixed(0) : calculatedResult["carbsCalories"].toString()) + ' kcal'), + if (calculatedResult["proteinGrams"] != null && calculatedResult["proteinCalories"] != null) + ('Protein: ' + ((calculatedResult["proteinGrams"] is num) ? (calculatedResult["proteinGrams"] as num).toStringAsFixed(0) : calculatedResult["proteinGrams"].toString()) + 'g = ' + ((calculatedResult["proteinCalories"] is num) ? (calculatedResult["proteinCalories"] as num).toStringAsFixed(0) : calculatedResult["proteinCalories"].toString()) + ' kcal'), + if (calculatedResult["fatGrams"] != null && calculatedResult["fatCalories"] != null) + ('Fat: ' + ((calculatedResult["fatGrams"] is num) ? (calculatedResult["fatGrams"] as num).toStringAsFixed(0) : calculatedResult["fatGrams"].toString()) + 'g = ' + ((calculatedResult["fatCalories"] is num) ? (calculatedResult["fatCalories"] as num).toStringAsFixed(0) : calculatedResult["fatCalories"].toString()) + ' kcal'), ], - color: Colors.green, + color: (calculatedResult["dietType"] == "Moderate Crabs" || calculatedResult["dietType"] == "USDA Guidelines ") ? Colors.green : Colors.orange, hide: true, - icon: Icons.scale, + icon: (calculatedResult["dietType"] == "Moderate Crabs" || calculatedResult["dietType"] == "USDA Guidelines ") ? Icons.scale : (calculatedResult["dietType"] == "Zone Diet" ? Icons.warning_rounded : Icons.trending_down), ), SizedBox(height: 16.h), - ] else if (calculatedResult["dietType"] == "Zone Diet") ...[ - calorieWidget( - title: calculatedResult["dietType"].toString(), - calories: calculatedResult["totalCalories"], - description: (() { - final total = calculatedResult["totalCalories"]; - final carbsG = calculatedResult["carbsGrams"]; - final carbsC = calculatedResult["carbsCalories"]; - final protG = calculatedResult["proteinGrams"]; - final protC = calculatedResult["proteinCalories"]; - final fatG = calculatedResult["fatGrams"]; - final fatC = calculatedResult["fatCalories"]; - final base = - 'Zone Diet (~40% carbs / 30% protein / 30% fat): emphasizes hormonal balance and portion control. May improve body composition and energy for some, but requires planning; personalize with a nutrition professional.'; - // if (total is num) { - // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; - // } - return base; - })(), - chipsItems: [ - ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), - ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), - ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), - ], - color: Colors.orange, - hide: true, - icon: Icons.warning_rounded, - ), ] ] else if (type == HealthCalculatorsTypeEnum.ovulation) ...[ Column( @@ -626,21 +521,23 @@ class _SelectClinicPageState extends State { SizedBox( width: 8.w, ), - calculatedResult["lastPeriodDate"].toString().toText16(color: AppColors.successColor, isBold: true), + (calculatedResult["lastPeriodDate"] ?? "").toString().toText16(color: AppColors.successColor, isBold: true), ], ), ], ), - SizedBox(height: 8.h), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: calculatedResult["lastPeriodDay"].toString(), - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), + if (calculatedResult["lastPeriodDay"] != null) ...[ + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["lastPeriodDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], SizedBox(height: 16.h), Row( children: [ @@ -648,19 +545,21 @@ class _SelectClinicPageState extends State { SizedBox( width: 8.w, ), - calculatedResult["ovulationDate"].toString().toText16(color: AppColors.successColor, isBold: true), + (calculatedResult["ovulationDate"] ?? "").toString().toText16(color: AppColors.successColor, isBold: true), ], ), - SizedBox(height: 8.h), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: calculatedResult["ovulationDay"].toString(), - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), + if (calculatedResult["ovulationDay"] != null) ...[ + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["ovulationDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], SizedBox(height: 16.h), appState.isArabic() ? "فترة الخصوبة".toText16(color: AppColors.textColor, isBold: true) : "Fertility Window".toText16(color: AppColors.textColor, isBold: true), SizedBox(height: 8.h), @@ -670,7 +569,7 @@ class _SelectClinicPageState extends State { SizedBox( width: 8.w, ), - calculatedResult["fertileWindowStart"].toString().toText13(color: AppColors.successColor, isBold: true), + (calculatedResult["fertileWindowStart"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), SizedBox( width: 4.w, ), @@ -678,15 +577,15 @@ class _SelectClinicPageState extends State { SizedBox( width: 4.w, ), - calculatedResult["fertileWindowEnd"].toString().toText13(color: AppColors.successColor, isBold: true), + (calculatedResult["fertileWindowEnd"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), ], ), SizedBox(height: 8.h), appState.isArabic() ? "هذه هي فترة خصوبتك. يساعد الحفاظ على نطاق صحي من السعرات الحرارية في دعم جسمك خلال هذه الفترة، مع مراعاة معدل الأيض الأساسي ومستوى نشاطك. مؤشر كتلة جسمك ضمن النطاق الصحي." - .toText12(height: 1.5, color: AppColors.textColorLight, isBold: true) + .toText12(height: 1.5, color: AppColors.textColorLight, isBold: true) : "This is your fertile window. Maintaining a healthy calorie range supports your body during this period, considering your Basal Metabolic Rate and activity. Your Body Mass Index is within the healthy range." - .toText12(height: 1.5, color: AppColors.textColorLight, isBold: true) + .toText12(height: 1.5, color: AppColors.textColorLight, isBold: true) ], ), ), @@ -745,156 +644,166 @@ class _SelectClinicPageState extends State { children: [ Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDueDateAccordion, width: 30.h, height: 30.h), SizedBox(width: 8.w), - calculatedResult["dueDate"].toString().toText16(color: AppColors.successColor, isBold: true), + (calculatedResult["dueDate"] ?? "").toString().toText16(color: AppColors.successColor, isBold: true), ], ), Expanded(child: SizedBox(width: 8.w)), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: calculatedResult["dueDateDay"].toString(), - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), + if (calculatedResult["dueDateDay"] != null) + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["dueDateDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), ], ), ], ), ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), - padding: EdgeInsets.all(16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - "First Trimester".toText16(color: AppColors.textColor, isBold: true), - Expanded(child: SizedBox(width: 8.h)), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "${calculatedResult["firstTrimester"]["weeks"]} Weeks", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), - SizedBox( - width: 8.w, - ), - calculatedResult["firstTrimester"]["start"].toString().toText13(color: AppColors.successColor, isBold: true), - SizedBox( - width: 4.w, - ), - "-".toText16(isBold: true, color: AppColors.successColor), - SizedBox( - width: 4.w, - ), - calculatedResult["firstTrimester"]["end"].toString().toText13(color: AppColors.successColor, isBold: true), - ], - ), - SizedBox(height: 8.h), - ], + if (calculatedResult["firstTrimester"] is Map) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "First Trimester".toText16(color: AppColors.textColor, isBold: true), + Expanded(child: SizedBox(width: 8.h)), + if (calculatedResult["firstTrimester"]?["weeks"] != null) + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["firstTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + (calculatedResult["firstTrimester"]?["start"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + (calculatedResult["firstTrimester"]?["end"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + ], + ), + SizedBox(height: 8.h), + ], + ), ), - ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), - padding: EdgeInsets.all(16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - "Second Trimester".toText16(color: AppColors.textColor, isBold: true), - Expanded(child: SizedBox(width: 8.h)), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "${calculatedResult["secondTrimester"]["weeks"]} Weeks", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), - SizedBox( - width: 8.w, - ), - calculatedResult["secondTrimester"]["start"].toString().toText13(color: AppColors.successColor, isBold: true), - SizedBox( - width: 4.w, - ), - "-".toText16(isBold: true, color: AppColors.successColor), - SizedBox( - width: 4.w, - ), - calculatedResult["secondTrimester"]["end"].toString().toText13(color: AppColors.successColor, isBold: true), - ], - ), - SizedBox(height: 8.h), - ], + ], + if (calculatedResult["secondTrimester"] is Map) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Second Trimester".toText16(color: AppColors.textColor, isBold: true), + Expanded(child: SizedBox(width: 8.h)), + if (calculatedResult["secondTrimester"]?["weeks"] != null) + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["secondTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + (calculatedResult["secondTrimester"]?["start"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + (calculatedResult["secondTrimester"]?["end"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + ], + ), + SizedBox(height: 8.h), + ], + ), ), - ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), - padding: EdgeInsets.all(16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - "Third Trimester".toText16(color: AppColors.textColor, isBold: true), - Expanded(child: SizedBox(width: 8.h)), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "${calculatedResult["thirdTrimester"]["weeks"]} Weeks", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDayAccordion, width: 30.h, height: 30.h), - SizedBox( - width: 8.w, - ), - calculatedResult["thirdTrimester"]["start"].toString().toText13(color: AppColors.successColor, isBold: true), - SizedBox( - width: 4.w, - ), - "-".toText16(isBold: true, color: AppColors.successColor), - SizedBox( - width: 4.w, - ), - calculatedResult["thirdTrimester"]["end"].toString().toText13(color: AppColors.successColor, isBold: true), - ], - ), - SizedBox(height: 8.h), - ], + ], + if (calculatedResult["thirdTrimester"] is Map) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Third Trimester".toText16(color: AppColors.textColor, isBold: true), + Expanded(child: SizedBox(width: 8.h)), + if (calculatedResult["thirdTrimester"]?["weeks"] != null) + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["thirdTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDayAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + (calculatedResult["thirdTrimester"]?["start"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + (calculatedResult["thirdTrimester"]?["end"] ?? "").toString().toText13(color: AppColors.successColor, isBold: true), + ], + ), + SizedBox(height: 8.h), + ], + ), ), - ), + ], ], ) ], @@ -921,6 +830,7 @@ class _SelectClinicPageState extends State { } Widget calorieWidget({required String title, required dynamic calories, required String description, required Color color, required IconData icon, bool hide = false, List? chipsItems}) { + final caloriesNum = (calories is num) ? calories : (double.tryParse(calories?.toString() ?? '') ?? 0); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), padding: EdgeInsets.all(16.w), @@ -932,7 +842,7 @@ class _SelectClinicPageState extends State { children: [ Row( children: [ - ('${calories.toStringAsFixed(0)}').toText32(color: getTextColor(title), isBold: true), + ('${caloriesNum.toStringAsFixed(0)}').toText32(color: getTextColor(title), isBold: true), SizedBox(width: 10.w), hide ? SizedBox() : 'kcal/day'.toText12(color: AppColors.inputLabelTextColor, isBold: true) ], @@ -947,14 +857,14 @@ class _SelectClinicPageState extends State { runSpacing: 4.0, // Vertical space between rows of chips children: chipsItems.map((item) { return CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: item, - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor) + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: item, + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor) .paddingOnly(bottom: 4.h); }).toList(), ), @@ -1117,8 +1027,8 @@ class _SelectClinicPageState extends State { // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, child: Consumer(builder: (context, data, __) { - return getRegionalSelectionWidget(data, context); - }), callBackFunc: () {}); + return getRegionalSelectionWidget(data, context); + }), callBackFunc: () {}); } Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) { diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart index 3f86e380..42445fab 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart @@ -45,89 +45,110 @@ class _HealthCalculatorDetailedPageState extends State.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.calories: + case HealthCalculatorsTypeEnum.bmr: + final map = provider.caloriesResultMap ?? (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.idealBodyWeight: + final map = provider.ibwResultMap ?? (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.bodyFat: + final map = provider.bodyFatResultMap ?? (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.crabsProteinFat: + final map = provider.macrosResultMap ?? (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.ovulation: + final map = (provider.ovulationResult is Map) + ? provider.ovulationResult + : (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.deliveryDueDate: + final map = (provider.deliveryResult is Map) + ? provider.deliveryResult + : (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.bloodSugar: + final map = (provider.bloodSugarResult is Map) + ? provider.bloodSugarResult + : (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.bloodCholesterol: + final map = (provider.bloodCholesterolResult is Map) + ? provider.bloodCholesterolResult + : (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + case HealthCalculatorsTypeEnum.triglycerides: + final map = (provider.triglyceridesResult is Map) + ? provider.triglyceridesResult + : (result is Map ? Map.from(result) : null); + calculatedResult = map != null + ? {...map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : null; + break; + } + }); }, ).paddingSymmetrical(20.w, 24.h), ); @@ -140,68 +161,68 @@ class _HealthCalculatorDetailedPageState extends State Date: Sun, 23 Aug 2026 15:54:28 +0300 Subject: [PATCH 40/73] feat: enhance appointment logic with user authentication checks and project ID retrieval --- .../book_appointment/select_clinic_page.dart | 19 +++++++++++++++++-- .../book_appointment/select_doctor_page.dart | 2 +- .../widgets/appointment_calendar.dart | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 5f76ab31..df597d0c 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1058,7 +1058,8 @@ class _SelectClinicPageState extends State { regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); var appointmentsViewModel = Provider.of(context, listen: false); if (appointmentsViewModel.selectedClinic.clinicID == 253) { - appointmentsViewModel.setProjectID(regionalViewModel.selectedHospital?.patientDoctorAppointmentList!.first.iD?.toString() ?? ""); + var id =getProjectId(regionalViewModel); + appointmentsViewModel.setProjectID(id); appointmentsViewModel.resetLaserData(); appointmentsViewModel.getLaserClinic(); regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); @@ -1067,8 +1068,9 @@ class _SelectClinicPageState extends State { } } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { var appointmentsViewModel = Provider.of(context, listen: false); + var id =getProjectId(regionalViewModel); regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); - appointmentsViewModel.setProjectID(regionalViewModel.selectedHospital?.hospitalList.first.iD?.toString() ?? ""); + appointmentsViewModel.setProjectID(id); regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); } }, @@ -1379,4 +1381,17 @@ class _SelectClinicPageState extends State { callBackFunc: () {}, ); } + + String getProjectId(AppointmentViaRegionViewmodel regionalViewModel) { + try { + if(regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.isNotEmpty ?? false) { + return regionalViewModel.selectedHospital!.patientDoctorAppointmentList?.first.projectID?.toString() ?? ""; + } else if(regionalViewModel.selectedHospital?.hospitalList.isNotEmpty ?? false) { + return regionalViewModel.selectedHospital!.hospitalList.first.iD?.toString() ?? ""; + } + } on Exception catch (e) { + + } + return ""; + } } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 661c9a30..cb2555f3 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -53,7 +53,7 @@ class _SelectDoctorPageState extends State { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { - if (appState.getAuthenticatedUser()!.age! > 12) { + if (appState.isAuthenticated && appState.getAuthenticatedUser()!.age! > 12) { bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); } else { bookAppointmentsViewModel.getDoctorsList(isNearest: false); diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index a95989a6..8d96c12c 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -278,7 +278,7 @@ class _AppointmentCalendarState extends State { bookAppointmentsViewModel.setProjectID(bookAppointmentsViewModel.selectedDoctor.projectID.toString()); bookAppointmentsViewModel.setSelectedClinic(selectedClinic); bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); - if(appState.getAuthenticatedUser()!.age! > 12) { + if(appState.isAuthenticated && appState.getAuthenticatedUser()!.age! > 12) { Navigator.of(context).push( CustomPageRoute( page: DentalChiefComplaintsPage(), From 02e5b07681a9a45dc60c727054e78e3132b42839 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 23 Aug 2026 16:54:45 +0300 Subject: [PATCH 41/73] updates --- .../book_appointments_repo.dart | 5 +++-- .../book_appointments_view_model.dart | 2 +- lib/features/paytabs/paytabs_view_model.dart | 2 +- lib/main.dart | 17 ++++++++++------- .../appointments/appointment_payment_page.dart | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 1e0b8739..a705d84a 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -32,7 +32,7 @@ abstract class BookAppointmentsRepo { Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>> getDoctorRatingDetails(int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}); + Future>> getDoctorRatingDetails(int doctorId, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}); Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); @@ -314,9 +314,10 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { } @override - Future>> getDoctorRatingDetails(int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future>> getDoctorRatingDetails(int doctorId, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { Map mapDevice = { "DoctorID": doctorId, + "ProjectID": projectID, "PatientID": 0, "License": true, "IsRegistered": true, diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 4831901d..dbed528e 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -806,7 +806,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { doctorDetailsList.clear(); notifyListeners(); - final result = await bookAppointmentsRepo.getDoctorRatingDetails(selectedDoctor.doctorID ?? 0, onSuccess: onSuccess, onError: onError); + final result = await bookAppointmentsRepo.getDoctorRatingDetails(selectedDoctor.doctorID ?? 0, selectedDoctor.projectID ?? 0, onSuccess: onSuccess, onError: onError); result.fold( (failure) async { diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index b2563452..3e36233d 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -320,7 +320,7 @@ class PayTabsViewModel extends ChangeNotifier { payfortResponseInsertModel.cardNumber = paytabsTransactionResponseModel.paymentInfo!.paymentDescription; payfortResponseInsertModel.cardBin = ""; payfortResponseInsertModel.installmentsNumber = 0; - payfortResponseInsertModel.status = paytabsTransactionResponseModel.isSuccess! ? "success" : "failed"; + payfortResponseInsertModel.status = paytabsTransactionResponseModel.isSuccess! ? "14" : "00"; payfortResponseInsertModel.merchantRef = transactionID; payfortResponseInsertModel.isRefund = false; payfortResponseInsertModel.remmeberMe = true; diff --git a/lib/main.dart b/lib/main.dart index b22bbb6b..223103ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,11 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/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/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart'; @@ -76,12 +79,12 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs -// class MyHttpOverrides extends HttpOverrides { -// @override -// HttpClient createHttpClient(SecurityContext? context) { -// return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; -// } -// } +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context)..badCertificateCallback = (X509Certificate cert, String host, int port) => true; + } +} Future callAppStateInitializations() async { final String deviceTypeId = (Platform.isIOS @@ -130,7 +133,7 @@ Future callInitializations() async { await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // HttpOverrides.global = MyHttpOverrides(); + HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); // Initialize Security Service early to catch threats before app logic runs diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 5c8bacf3..fb2c3043 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -649,9 +649,9 @@ class _AppointmentPaymentPageState extends State { doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(), transactionID: transID, amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), - fileNumber: widget.patientAppointmentHistoryResponseModel.patientID, + fileNumber: widget.patientAppointmentHistoryResponseModel.patientID.toString(), serviceID: "2", - projectID: widget.patientAppointmentHistoryResponseModel.projectID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID.toString(), isSchedule: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (PaytabsTransactionResponseModel transactionData) async { tamaraOrderID = transactionData.cartID!; From 0772beed1b0f3f5bb5d1655627207bd9eb69542a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 24 Aug 2026 16:15:01 +0300 Subject: [PATCH 42/73] PayTabs Tamara updates --- lib/core/paytabs_config.dart | 5 + lib/features/paytabs/paytabs_view_model.dart | 7 +- .../appointment_payment_page.dart | 129 +++++++---------- .../immediate_livecare_payment_page.dart | 81 +++++++++-- .../book_appointment/select_clinic_page.dart | 2 +- .../book_appointment/widgets/doctor_card.dart | 2 +- .../wallet_payment_confirm_page.dart | 10 +- .../ancillary_order_payment_page.dart | 132 +++--------------- lib/presentation/todo_section/todo_page.dart | 1 + lib/splashPage.dart | 5 + 10 files changed, 173 insertions(+), 201 deletions(-) diff --git a/lib/core/paytabs_config.dart b/lib/core/paytabs_config.dart index 4d90ee8f..b1ba0dd4 100644 --- a/lib/core/paytabs_config.dart +++ b/lib/core/paytabs_config.dart @@ -3,6 +3,11 @@ class PaymentSdkDefaultConfig { static const String profileID = "119730"; static const String serverKey = "SWJ9LD22MT-JLJZLBKTLK-WGWTLWGTWR"; static const String clientKey = "C7K2DQ-PDDN6B-69B2K7-P9GB9T"; + + // static const String profileID = "135577"; + // static const String serverKey = "SDJ9LD22LR-J9LRBKZDRD-ZRZKWDWBRR"; + // static const String clientKey = "CBK2DQ-PDPN6P-BR2K9D-VPBTB2"; + static const String defaultCurrency = "SAR"; static const double defaultAmount = 10.0; static const String defaultMerchantCountryCode = "SA"; diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index 3e36233d..1418cc55 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -57,7 +57,8 @@ class PayTabsViewModel extends ChangeNotifier { amount: amount.toDouble(), showBillingInfo: false, forceShippingInfo: false, - currencyCode: PaymentSdkDefaultConfig.defaultCurrency, + // currencyCode: PaymentSdkDefaultConfig.defaultCurrency, + currencyCode: getIt.get().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", merchantCountryCode: PaymentSdkDefaultConfig.defaultMerchantCountryCode, billingDetails: BillingDetails( "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", @@ -126,7 +127,8 @@ class PayTabsViewModel extends ChangeNotifier { amount: amount.toDouble(), showBillingInfo: false, forceShippingInfo: false, - currencyCode: PaymentSdkDefaultConfig.defaultCurrency, + // currencyCode: PaymentSdkDefaultConfig.defaultCurrency, + currencyCode: getIt.get().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", merchantCountryCode: PaymentSdkDefaultConfig.defaultMerchantCountryCode, billingDetails: BillingDetails("${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"), @@ -398,7 +400,6 @@ class PayTabsViewModel extends ChangeNotifier { tamaraRequestModel.accessCode = getIt.get().getAuthenticatedUser()!.mobileNumber!; tamaraRequestModel.appointmentNo = appointmentNo; tamaraRequestModel.customerName = "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}"; - ; tamaraRequestModel.fileNumber = fileNumber; tamaraRequestModel.patientOutSA = getIt.get().getAuthenticatedUser()!.outSa == 1 ? true : false; tamaraRequestModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index fb2c3043..6e9bb7b6 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -632,7 +632,6 @@ class _AppointmentPaymentPageState extends State { ).paddingSymmetrical(16.h, 16.h), ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "TAMARA"; - transID = Utils.getAppointmentTransID( widget.patientAppointmentHistoryResponseModel.projectID, widget.patientAppointmentHistoryResponseModel.clinicID, @@ -660,86 +659,64 @@ class _AppointmentPaymentPageState extends State { await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); await payfortViewModel.markAppointmentAsTamaraPaid( projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); - await myAppointmentsViewModel.createAdvancePayment( - paymentMethodName: selectedPaymentMethod, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - payedAmount: num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), - paymentReference: transactionData.transactionReference!, - patientID: appState.getAuthenticatedUser()!.patientId.toString(), - patientType: appState.getAuthenticatedUser()!.patientType!, - onSuccess: (value) async { - print(value); - await myAppointmentsViewModel.addAdvanceNumberRequest( - advanceNumber: "Tamara-Advance-0000", - paymentReference: transactionData.transactionReference!, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - onSuccess: (value) async { - if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { - //TODO: Implement LiveCare Check-In API Call - await myAppointmentsViewModel.insertLiveCareVIDARequest( - clientRequestID: transID, - patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, - onSuccess: (apiResponse) { - Future.delayed(Duration(milliseconds: 500), () { - myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); - LoaderBottomSheet.hideLoader(); + await myAppointmentsViewModel.addAdvanceNumberRequest( + advanceNumber: "Tamara-Advance-0000", + paymentReference: transactionData.transactionReference!, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + onSuccess: (value) async { + if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { + //TODO: Implement LiveCare Check-In API Call + await myAppointmentsViewModel.insertLiveCareVIDARequest( + clientRequestID: transID, + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); + LoaderBottomSheet.hideLoader(); - getIt.get().setIsWalletAmountToBeLoaded(true); - getIt.get().initHabibWalletProvider(); - getIt.get().getPatientBalanceAmount(); + getIt.get().setIsWalletAmountToBeLoaded(true); + getIt.get().initHabibWalletProvider(); + getIt.get().getPatientBalanceAmount(); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); - }, - onError: (error) {}); - } else { - await myAppointmentsViewModel.generateAppointmentQR( - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, - onSuccess: (apiResponse) { - Future.delayed(Duration(milliseconds: 500), () { - myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }, + onError: (error) {}); + } else { + await myAppointmentsViewModel.generateAppointmentQR( + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); - getIt.get().setIsWalletAmountToBeLoaded(true); - getIt.get().initHabibWalletProvider(); - getIt.get().getPatientBalanceAmount(); + getIt.get().setIsWalletAmountToBeLoaded(true); + getIt.get().initHabibWalletProvider(); + getIt.get().getPatientBalanceAmount(); - LoaderBottomSheet.hideLoader(); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); + LoaderBottomSheet.hideLoader(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); }); - } - }); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err.toString()), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }, onError: (err) async { + }); + } + }); + }, onError: (err) async { await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); showCommonBottomSheetWithoutHeight( context, 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 9ef3f8fb..2ab1e303 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; +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/api_consts.dart'; @@ -60,6 +61,7 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) async {}, + ); + } else { + openPaymentURL("TAMARA"); + } }) : SizedBox.shrink(), ], @@ -524,17 +579,24 @@ class _ImmediateLiveCarePaymentPageState extends State { bookAppointmentsViewModel.setSelectedClinic(clinic); bookAppointmentsViewModel.setIsDoctorsListLoading(true); searchEditingController.text = ""; - if (clinic.isLiveCareClinicAndOnline ?? false) { + if (appState.isAuthenticated && (clinic.isLiveCareClinicAndOnline ?? false)) { Navigator.of(context).push( CustomPageRoute( page: SelectLivecareClinicPage( diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 8a48465a..d2f431a9 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -86,7 +86,7 @@ class DoctorCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, applyThemeColor: false), SizedBox(height: 2.h), - "${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + "${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate ?? 5.0}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100).toShimmer2(isShow: isLoading), diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index ce260ca0..1ddaaec9 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:amazon_payfort/amazon_payfort.dart'; +import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -54,6 +55,7 @@ class _WalletPaymentConfirmPageState extends State { @override void initState() { + Clarity.setCurrentScreenName('Habib Wallet Payment Page'); scheduleMicrotask(() { payfortViewModel.initPayfortViewModel(); }); @@ -509,9 +511,11 @@ class _WalletPaymentConfirmPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.totalAmountToPay.tr(context: context).toText16(isBold: true), - Utils.getPaymentAmountWithSymbol( - NumberFormat.decimalPattern().format(habibWalletVM.walletRechargeAmount).toString().toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 15.h, - isSaudiCurrency: true), + ClarityUnmask( + child: Utils.getPaymentAmountWithSymbol( + NumberFormat.decimalPattern().format(habibWalletVM.walletRechargeAmount).toString().toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 15.h, + isSaudiCurrency: true), + ), ], ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 12.h), diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index 36a2d7c5..e7553df5 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; +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'; @@ -77,25 +78,10 @@ class _AncillaryOrderPaymentPageState extends State { @override void initState() { + Clarity.setCurrentScreenName('Ancillary Payment Page'); scheduleMicrotask(() { payfortViewModel.initPayfortViewModel(); payfortViewModel.setIsApplePayConfigurationLoading(false); - - myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { - // if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) { - if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && - myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.minLimit?.amount != null && - myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.maxLimit?.amount != null) { - if (widget.totalAmount >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && - widget.totalAmount <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { - if (mounted) { - setState(() { - isShowTamara = true; - }); - } - } - } - }); }); super.initState(); } @@ -393,84 +379,6 @@ class _AncillaryOrderPaymentPageState extends State { } }), SizedBox(height: 16.h), - isShowTamara - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: false, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h).toShimmer2(isShow: false), - SizedBox(height: 16.h), - LocaleKeys.tamara.tr(context: context).toText16(isBold: true).toShimmer2(isShow: false), - ], - ), - SizedBox(width: 8.h), - const Spacer(), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.blackColor, - width: 18.h, - height: 13.h, - fit: BoxFit.contain, - ).toShimmer2(isShow: false), - ), - ], - ).paddingSymmetrical(16.h, 16.h), - ).paddingSymmetrical(24.h, 0.h).onPress(() { - selectedPaymentMethod = "TAMARA"; - transID = Utils.getAppointmentTransID( - widget.projectID, - widget.clinicID, - widget.appointmentNoVida, - ); - if (appState.isPaytabsEnabled) { - paytabsViewModel.setAlternatePaymentConfiguration("Ancillary Order Payment", widget.totalAmount); - paytabsViewModel.startAlternatePayment( - liveServiceID: "0", - appointmentDate: DateUtil.convertDateToString(widget.appointmentDate!), - appointmentNo: widget.appointmentNoVida.toString(), - doctorID: "0", - transactionID: transID, - amount: widget.totalAmount.toString(), - fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), - serviceID: "2", - projectID: widget.projectID.toString(), - isSchedule: false, - onSuccess: (PaytabsTransactionResponseModel transactionData) async { - tamaraOrderID = transactionData.cartID!; - // transID = transactionData.transactionReference!; - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); - await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); - _createAdvancePayment( - paymentAmount: widget.totalAmount, - paymentReference: transactionData.transactionReference!, - paymentMethod: selectedPaymentMethod, - ); - }, - onError: (err) async { - await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err.toString()), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - } else { - _openPaymentURL("tamara"); - } - }) - : SizedBox.shrink(), ], ), ), @@ -514,11 +422,13 @@ class _AncillaryOrderPaymentPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.amountBeforeTax.tr(context: context).toText14(isBold: true), - Utils.getPaymentAmountWithSymbol( - amountBeforeTax.toStringAsFixed(2).toText16(isBold: true), - AppColors.blackColor, - 13, - isSaudiCurrency: true, + ClarityUnmask( + child: Utils.getPaymentAmountWithSymbol( + amountBeforeTax.toStringAsFixed(2).toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true, + ), ), ], ).paddingSymmetrical(24.h, 0.h), @@ -528,11 +438,13 @@ class _AncillaryOrderPaymentPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor), - Utils.getPaymentAmountWithSymbol( - taxAmount.toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), - AppColors.greyTextColor, - 13, - isSaudiCurrency: true, + ClarityUnmask( + child: Utils.getPaymentAmountWithSymbol( + taxAmount.toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), + AppColors.greyTextColor, + 13, + isSaudiCurrency: true, + ), ), ], ).paddingSymmetrical(24.h, 0.h), @@ -544,11 +456,13 @@ class _AncillaryOrderPaymentPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "".toText14(isBold: true), - Utils.getPaymentAmountWithSymbol( - widget.totalAmount.toStringAsFixed(2).toText24(isBold: true), - AppColors.blackColor, - 17, - isSaudiCurrency: true, + ClarityUnmask( + child: Utils.getPaymentAmountWithSymbol( + widget.totalAmount.toStringAsFixed(2).toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true, + ), ), ], ).paddingSymmetrical(24.h, 0.h), diff --git a/lib/presentation/todo_section/todo_page.dart b/lib/presentation/todo_section/todo_page.dart index 2f1061d2..7f59b2d2 100644 --- a/lib/presentation/todo_section/todo_page.dart +++ b/lib/presentation/todo_section/todo_page.dart @@ -34,6 +34,7 @@ class _ToDoPageState extends State { scheduleMicrotask(() async { if (appState.isAuthenticated) { await todoSectionViewModel.initializeTodoSectionViewModel(); + todoSectionViewModel.setProcessingPayment(false); } }); super.initState(); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 8b39bf28..fe223fc2 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -13,9 +13,11 @@ 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/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/main.dart'; import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; @@ -58,6 +60,9 @@ class _SplashScreenState extends State { () async { ApiConsts.setBackendURLs(); PushNotificationHandler().init(context); // Asyncronously + // if (ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat) { + // HttpOverrides.global = MyHttpOverrides(); + // } }, ); From 112c254416ff91cf6d26ce9d9cf49c829ba7697e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 25 Aug 2026 10:31:32 +0300 Subject: [PATCH 43/73] Update to stores VersionID 21.9 --- lib/core/api_consts.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 7d3b97d4..094fa680 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/pubspec.yaml b/pubspec.yaml index 0c5d78dd..e471cada 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev #version: 0.0.45+46 -version: 0.0.14+3 +version: 0.0.15+2 environment: sdk: ">=3.6.0 <4.0.0" From f6459142e187b50c40a3cf9ae5974559a717129e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 25 Aug 2026 17:10:42 +0300 Subject: [PATCH 44/73] Paytabs dynamic keys implemented --- .../immediate_livecare_repo.dart | 2 - lib/features/payfort/payfort_repo.dart | 2 +- lib/features/paytabs/paytabs_view_model.dart | 54 +++++++++++++++---- .../appointment_payment_page.dart | 44 ++++++++------- .../immediate_livecare_payment_page.dart | 34 ++++++------ .../waiting_appointment_payment_page.dart | 6 +-- .../er_online_checkin_payment_page.dart | 6 +-- .../wallet_payment_confirm_page.dart | 13 +++-- .../ancillary_order_payment_page.dart | 30 +++++------ 9 files changed, 111 insertions(+), 80 deletions(-) diff --git a/lib/features/immediate_livecare/immediate_livecare_repo.dart b/lib/features/immediate_livecare/immediate_livecare_repo.dart index 6726a09b..e07d6293 100644 --- a/lib/features/immediate_livecare/immediate_livecare_repo.dart +++ b/lib/features/immediate_livecare/immediate_livecare_repo.dart @@ -101,9 +101,7 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final respObject = response['GetERAppointmentFeesList']; - final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); - apiResponse = GenericApiModel( messageStatus: messageStatus, statusCode: statusCode, diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart index 9262d662..6826478c 100644 --- a/lib/features/payfort/payfort_repo.dart +++ b/lib/features/payfort/payfort_repo.dart @@ -58,7 +58,7 @@ class PayfortRepoImp implements PayfortRepo { try { final list = response; if (list == null || list.isEmpty) { - throw Exception("payfort list is empty"); + throw Exception("No project configuration found."); } final applePayConfigurationResponse = PayfortProjectDetailsRespModel.fromJson(list[0]); diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index 1418cc55..a7894f07 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -17,6 +17,7 @@ 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/payfort/models/payfort_project_details_resp_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.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'; @@ -32,19 +33,26 @@ class PayTabsViewModel extends ChangeNotifier { final ErrorHandlerService errorHandlerService; late PaytabsTransactionResponseModel paytabsTransactionResponseModel; + PayfortProjectDetailsRespModel? paytabsProjectDetailsRespModel; + PayTabsViewModel({ required this.appState, required this.payfortRepo, required this.errorHandlerService, }); - setPaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) { + setPaymentConfiguration(String paymentDescription, num amount, int serviceID, int projectID, {bool isAddCard = false}) async { String cartID = DateTime.now().millisecondsSinceEpoch.toString(); + + LoaderBottomSheet.showLoader(); + + await getPayTabsConfigurations(serviceId: serviceID, projectId: projectID); + paymentConfiguration = PaymentSdkConfigurationDetails( - profileId: PaymentSdkDefaultConfig.profileID, - serverKey: PaymentSdkDefaultConfig.serverKey, - clientKey: PaymentSdkDefaultConfig.clientKey, - transactionType: PaymentSdkTransactionType.SALE, + profileId: paytabsProjectDetailsRespModel!.merchantIdentifier, + serverKey: paytabsProjectDetailsRespModel!.accessCode, + clientKey: paytabsProjectDetailsRespModel!.shaResponse, + transactionType: PaymentSdkTransactionType.SALE, cartId: cartID.substring((cartID.length - 5), cartID.length), cartDescription: paymentDescription, merchantName: PaymentSdkDefaultConfig.defaultMerchantName, @@ -112,12 +120,17 @@ class PayTabsViewModel extends ChangeNotifier { notifyListeners(); } - setAlternatePaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) { + setAlternatePaymentConfiguration(String paymentDescription, num amount, int serviceID, int projectID, {bool isAddCard = false}) async { String cartID = DateTime.now().millisecondsSinceEpoch.toString(); + + LoaderBottomSheet.showLoader(); + + await getPayTabsConfigurations(serviceId: serviceID, projectId: projectID); + paymentConfiguration = PaymentSdkConfigurationDetails( - profileId: PaymentSdkDefaultConfig.profileID, - serverKey: PaymentSdkDefaultConfig.serverKey, - clientKey: PaymentSdkDefaultConfig.clientKey, + profileId: paytabsProjectDetailsRespModel!.merchantIdentifier, + serverKey: paytabsProjectDetailsRespModel!.accessCode, + clientKey: paytabsProjectDetailsRespModel!.shaResponse, transactionType: PaymentSdkTransactionType.SALE, cartId: cartID.substring((cartID.length - 5), cartID.length), cartDescription: paymentDescription, @@ -260,7 +273,7 @@ class PayTabsViewModel extends ChangeNotifier { bool _callbackFired = false; // prevent duplicate callback invocations try { - LoaderBottomSheet.showLoader(); + // LoaderBottomSheet.showLoader(); // Create PayfortRequestInsertModel (same as Payfort implementation) PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel(); @@ -382,7 +395,7 @@ class PayTabsViewModel extends ChangeNotifier { required bool isSchedule, Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) async { - LoaderBottomSheet.showLoader(); + // LoaderBottomSheet.showLoader(); TamaraRequestModel tamaraRequestModel = TamaraRequestModel(); @@ -448,4 +461,23 @@ class PayTabsViewModel extends ChangeNotifier { } }); } + + Future getPayTabsConfigurations({int? serviceId, int? projectId, int integrationId = 5, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.getPayfortConfigurations(serviceId: serviceId, projectId: projectId, integrationId: integrationId); + + 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) { + paytabsProjectDetailsRespModel = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 6e9bb7b6..bf2c0fcd 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -150,10 +150,11 @@ class _AppointmentPaymentPageState extends State { widget.patientAppointmentHistoryResponseModel.clinicID, widget.patientAppointmentHistoryResponseModel.appointmentNo, ); - paytabsViewModel.setPaymentConfiguration( - "Appointment Payment", - num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), - ); + await paytabsViewModel.setPaymentConfiguration( + "Appointment Payment", + num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), + ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), + widget.patientAppointmentHistoryResponseModel.projectID); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -179,7 +180,6 @@ class _AppointmentPaymentPageState extends State { patientID: appState.getAuthenticatedUser()!.patientId.toString(), patientType: appState.getAuthenticatedUser()!.patientType!, onSuccess: (value) async { - print(value); await myAppointmentsViewModel.addAdvanceNumberRequest( advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() @@ -263,7 +263,6 @@ class _AppointmentPaymentPageState extends State { orderDescription: "Appointment Payment", amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), onSucceeded: (paymentResult) async { - print("Payment Success Data: ${paymentResult.toString()}"); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, @@ -275,7 +274,6 @@ class _AppointmentPaymentPageState extends State { patientID: appState.getAuthenticatedUser()!.patientId.toString(), patientType: appState.getAuthenticatedUser()!.patientType!, onSuccess: (value) async { - print(value); await myAppointmentsViewModel.addAdvanceNumberRequest( advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() @@ -391,10 +389,11 @@ class _AppointmentPaymentPageState extends State { widget.patientAppointmentHistoryResponseModel.clinicID, widget.patientAppointmentHistoryResponseModel.appointmentNo, ); - paytabsViewModel.setPaymentConfiguration( - "Appointment Payment", - num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), - ); + await paytabsViewModel.setPaymentConfiguration( + "Appointment Payment", + num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), + ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), + widget.patientAppointmentHistoryResponseModel.projectID); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -420,7 +419,6 @@ class _AppointmentPaymentPageState extends State { patientID: appState.getAuthenticatedUser()!.patientId.toString(), patientType: appState.getAuthenticatedUser()!.patientType!, onSuccess: (value) async { - print(value); await myAppointmentsViewModel.addAdvanceNumberRequest( advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() @@ -504,7 +502,6 @@ class _AppointmentPaymentPageState extends State { amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(), orderDescription: "Appointment Payment", onSucceeded: (paymentResult) async { - print("Payment Success Data: ${paymentResult.toString()}"); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await myAppointmentsViewModel.createAdvancePayment( paymentMethodName: selectedPaymentMethod, @@ -516,7 +513,6 @@ class _AppointmentPaymentPageState extends State { patientID: appState.getAuthenticatedUser()!.patientId.toString(), patientType: appState.getAuthenticatedUser()!.patientType!, onSuccess: (value) async { - print(value); await myAppointmentsViewModel.addAdvanceNumberRequest( advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() @@ -640,7 +636,10 @@ class _AppointmentPaymentPageState extends State { if (appState.isPaytabsEnabled) { paytabsViewModel.setAlternatePaymentConfiguration( - "Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString())); + "Appointment Payment", + num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), + ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), + widget.patientAppointmentHistoryResponseModel.projectID); paytabsViewModel.startAlternatePayment( liveServiceID: "0", appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, @@ -1121,10 +1120,13 @@ class _AppointmentPaymentPageState extends State { ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); - await payfortViewModel.getPayfortConfigurations( - serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), - projectId: widget.patientAppointmentHistoryResponseModel.projectID, - integrationId: 2); + if (!appState.isPaytabsEnabled) { + await payfortViewModel.getPayfortConfigurations( + serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 2); + } else { + await paytabsViewModel.setPaymentConfiguration("Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), + ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), widget.patientAppointmentHistoryResponseModel.projectID); + } applePayInsertRequest.clientRequestID = transID; applePayInsertRequest.clinicID = widget.patientAppointmentHistoryResponseModel.clinicID; @@ -1173,10 +1175,6 @@ class _AppointmentPaymentPageState extends State { if (appState.isPaytabsEnabled) { selectedPaymentMethod = "VISA"; LoaderBottomSheet.hideLoader(); - paytabsViewModel.setPaymentConfiguration( - "Appointment Payment", - num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()), - ); paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await myAppointmentsViewModel.createAdvancePayment( 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 2ab1e303..c51ac120 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -137,10 +137,8 @@ class _ImmediateLiveCarePaymentPageState extends State paytabsViewModel.setPaymentConfiguration( "ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, - ); + ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -238,7 +238,7 @@ class _ErOnlineCheckinPaymentPageState extends State paytabsViewModel.setPaymentConfiguration( "ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, - ); + ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -533,7 +533,7 @@ class _ErOnlineCheckinPaymentPageState extends State paytabsViewModel.setPaymentConfiguration( "ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, - ); + ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); if (emergencyServicesViewModel.isERBookAppointment) { diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 1ddaaec9..c407e6ad 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -116,7 +116,8 @@ class _WalletPaymentConfirmPageState extends State { selectedPaymentMethod = "MADA"; if (appState.isPaytabsEnabled) { transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber)); - paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount); + await paytabsViewModel.setPaymentConfiguration( + "Advance Payment", habibWalletVM.walletRechargeAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), habibWalletVM.selectedHospital!.iD!); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -313,7 +314,8 @@ class _WalletPaymentConfirmPageState extends State { selectedPaymentMethod = "VISA"; if (appState.isPaytabsEnabled) { transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber)); - paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount); + await paytabsViewModel.setPaymentConfiguration( + "Advance Payment", habibWalletVM.walletRechargeAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), habibWalletVM.selectedHospital!.iD!); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -543,7 +545,11 @@ class _WalletPaymentConfirmPageState extends State { transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber)); - await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 2); + if(!appState.isPaytabsEnabled) { + await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 2); + } else { + await paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), habibWalletVM.selectedHospital!.iD!); + } applePayInsertRequest.clientRequestID = transID; applePayInsertRequest.clinicID = 0; @@ -592,7 +598,6 @@ class _WalletPaymentConfirmPageState extends State { if(appState.isPaytabsEnabled) { selectedPaymentMethod = "VISA"; LoaderBottomSheet.hideLoader(); - paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount); paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { LoaderBottomSheet.showLoader(); await habibWalletVM.HISCreateAdvancePayment( diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index e7553df5..365dff2b 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -150,10 +150,7 @@ class _AncillaryOrderPaymentPageState extends State { widget.projectID, appState.getAuthenticatedUser()!.patientId!, ); - paytabsViewModel.setPaymentConfiguration( - "Ancillary Orders Payment", - widget.totalAmount, - ); + await paytabsViewModel.setPaymentConfiguration("Ancillary Orders Payment", widget.totalAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), widget.projectID); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, liveServiceID: "0", @@ -289,10 +286,7 @@ class _AncillaryOrderPaymentPageState extends State { widget.projectID, appState.getAuthenticatedUser()!.patientId!, ); - paytabsViewModel.setPaymentConfiguration( - "Ancillary Orders Payment", - widget.totalAmount, - ); + await paytabsViewModel.setPaymentConfiguration("Ancillary Orders Payment", widget.totalAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), widget.projectID); paytabsViewModel.startCardPayment(paymentOption: selectedPaymentMethod, liveServiceID: "0", isScheduleLiveCare: "false", @@ -721,11 +715,13 @@ class _AncillaryOrderPaymentPageState extends State { transID = Utils.getAdvancePaymentTransID(widget.projectID, user!.patientId!); ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); - await payfortViewModel.getPayfortConfigurations( - serviceId: ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum(), - projectId: widget.projectID, - integrationId: 2, - ); + if (!appState.isPaytabsEnabled) { + await payfortViewModel.getPayfortConfigurations( + serviceId: ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum(), + projectId: widget.projectID, + integrationId: 2, + ); + } applePayInsertRequest.clientRequestID = transID; applePayInsertRequest.clinicID = 0; @@ -770,6 +766,10 @@ class _AncillaryOrderPaymentPageState extends State { applePayInsertRequest.returnURL = ""; try { + if (appState.isPaytabsEnabled) { + await paytabsViewModel.setPaymentConfiguration("Ancillary Orders Payment", widget.totalAmount, ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), widget.projectID); + } + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest); } catch (error) { log("Apple Pay Insert Request Failed: $error"); @@ -787,10 +787,6 @@ class _AncillaryOrderPaymentPageState extends State { if(appState.isPaytabsEnabled) { selectedPaymentMethod = "VISA"; LoaderBottomSheet.hideLoader(); - paytabsViewModel.setPaymentConfiguration( - "Ancillary Orders Payment", - widget.totalAmount, - ); paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { final paymentAmount = widget.totalAmount; final fortId = transactionData.transactionReference; From e82ba228d515a884f1fd13a12405a54b4c6e5d74 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 26 Aug 2026 11:14:34 +0300 Subject: [PATCH 45/73] PayTabs implementation contd. --- .../appointments/appointment_payment_page.dart | 11 ++++++----- .../immediate_livecare_payment_page.dart | 17 ++++++++--------- .../wallet_payment_confirm_page.dart | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index bf2c0fcd..85a606b3 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -1162,12 +1162,13 @@ class _AppointmentPaymentPageState extends State { applePayInsertRequest.isMobSDK = true; applePayInsertRequest.merchantReference = transID; - applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.merchantIdentifier = + appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.merchantIdentifier : payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; applePayInsertRequest.commandType = "PURCHASE"; - applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; - applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; - applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; - applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.signature = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.signature : payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.accessCode : payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.shaRequest : payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.shaResponse : payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; applePayInsertRequest.returnURL = ""; //TODO: Need to pass dynamic params to the Apple Pay instead of static values 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 c51ac120..692e8b44 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -147,7 +147,7 @@ class _ImmediateLiveCarePaymentPageState extends State { applePayInsertRequest.isMobSDK = true; applePayInsertRequest.merchantReference = transID; - applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.merchantIdentifier = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.merchantIdentifier : payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; applePayInsertRequest.commandType = "PURCHASE"; - applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; - applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; - applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; - applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.signature = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.signature : payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.accessCode : payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.shaRequest : payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = appState.isPaytabsEnabled ? paytabsViewModel.paytabsProjectDetailsRespModel!.shaResponse : payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; applePayInsertRequest.returnURL = ""; //TODO: Need to pass dynamic params to the Apple Pay instead of static values From 26c20e110378d7d26c6a6d90cf33d6f344a472df Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 30 Aug 2026 10:06:26 +0300 Subject: [PATCH 46/73] appointment checkin privilege applied --- .../widgets/appointment_checkin_bottom_sheet.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index dd6cf8eb..acf2a274 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -45,8 +45,9 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - checkInOptionCard( - AppAssets.checkin_location_icon, + Utils.havePrivilege(102) + ? checkInOptionCard( + AppAssets.checkin_location_icon, LocaleKeys.liveLocation.tr(context: context), LocaleKeys.verifyYourLocationAtHospital.tr(context: context), ).onPress(() { @@ -71,9 +72,10 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { }, isFullScreen: false); } }); - }), + }) + : SizedBox.shrink(), // SizedBox(height: 16.h), - // checkInOptionCard(O + // checkInOptionCard( // AppAssets.checkin_nfc_icon, // LocaleKeys.nfcNearFieldCommunication.tr(context: context), // LocaleKeys.scanPhoneViaNFC.tr(context: context), From 81bf111c2e47fd3297137a1751d6c8ca67b17b5e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 30 Aug 2026 10:29:51 +0300 Subject: [PATCH 47/73] updates --- .../widgets/appointment_doctor_card.dart | 109 ++++++++++++------ lib/presentation/home/landing_page.dart | 31 +++-- 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 8738a449..dc7bb0af 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -173,39 +173,39 @@ class AppointmentDoctorCard extends StatelessWidget { ? SizedBox.shrink() : Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown) .onPress(() async { - DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); - selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; - selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; - selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; - selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; - selectedDoctor.nationalityFlagURL = ""; - selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; - selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; - selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; - selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; - selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; - - getIt.get().setSelectedDoctor(selectedDoctor); - LoaderBottomSheet.showLoader(); - await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage( - isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false), - ), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); + // DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); + // selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; + // selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; + // selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; + // selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; + // selectedDoctor.nationalityFlagURL = ""; + // selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; + // selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; + // selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; + // selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; + // selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; + // + // getIt.get().setSelectedDoctor(selectedDoctor); + // LoaderBottomSheet.showLoader(); + // await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { + // LoaderBottomSheet.hideLoader(); + // Navigator.of(context).push( + // CustomPageRoute( + // page: DoctorProfilePage( + // isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false), + // ), + // ), + // ); + // }, onError: (err) { + // LoaderBottomSheet.hideLoader(); + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getErrorWidget(loadingText: err), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + // }); }) ], ), @@ -220,7 +220,48 @@ class AppointmentDoctorCard extends StatelessWidget { ], ), ), - ); + ).onPress(() async { + if ((patientAppointmentHistoryResponseModel.isLiveCareAppointment! || + patientAppointmentHistoryResponseModel.isExecludeDoctor! || + patientAppointmentHistoryResponseModel.isClinicReBookingAllowed! == false || + patientAppointmentHistoryResponseModel.isActiveDoctor! == false || + !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID))) { + } else { + DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); + selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; + selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; + selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; + selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; + selectedDoctor.nationalityFlagURL = ""; + selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; + selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; + selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; + selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; + selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; + + getIt.get().setSelectedDoctor(selectedDoctor); + LoaderBottomSheet.showLoader(); + await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage( + isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false), + ), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } + }); } Widget getAppointmentActionButtons(bool isAppointmentArrived) { diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index bf1a7b62..1f8def35 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -357,21 +357,7 @@ class _LandingPageState extends State { // spacing: 18.h, children: [ Stack(clipBehavior: Clip.none, children: [ - if (appState.isAuthenticated) - Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h).onPress(() async { - if (appState.isAuthenticated) { - notificationsViewModel.setNotificationStatusID(2); - notificationsViewModel.getAllNotifications(); - Navigator.of(context).push( - CustomPageRoute( - page: NotificationsListPage(), - // page: LoginScreen(), - ), - ); - } else { - await authVM.onLoginPressed(); - } - }), + if (appState.isAuthenticated) Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h), (appState.isAuthenticated && todoSectionVM.notificationsCount != null && todoSectionVM.notificationsCount != "0") ? Positioned( // right: appState.isArabic() ? 8.w : -8.w, @@ -399,7 +385,20 @@ class _LandingPageState extends State { ), ) : SizedBox.shrink(), - ]), + ]).onPress(() async { + if (appState.isAuthenticated) { + notificationsViewModel.setNotificationStatusID(2); + notificationsViewModel.getAllNotifications(); + Navigator.of(context).push( + CustomPageRoute( + page: NotificationsListPage(), + // page: LoginScreen(), + ), + ); + } else { + await authVM.onLoginPressed(); + } + }), SizedBox( width: 24.w, ), From e80b6edf1e6e5e2848e7b9cb283d91c9b28d10f1 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 30 Aug 2026 17:19:58 +0300 Subject: [PATCH 48/73] vital sign for appointment done. --- lib/core/api/api_client.dart | 2 +- lib/core/app_assets.dart | 1 + .../hmg_services/hmg_services_repo.dart | 7 +- .../hmg_services/hmg_services_view_model.dart | 19 +++- .../appointment_details_page.dart | 76 ++++++++++++++++ .../vital_sign/vital_sign_details_page.dart | 76 +++++++++++----- .../vital_sign/vital_sign_page.dart | 87 +++++++++++-------- 7 files changed, 204 insertions(+), 64 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 3a5ff281..4db0cf8b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - body['TokenID'] = "@dm!n"; + // body['TokenID'] = "@dm!n"; // body['PatientID'] = 1307867; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index b4611890..d5f46e82 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -110,6 +110,7 @@ class AppAssets { static const String report_icon = '$svgBasePath/report_icon.svg'; static const String radiology_icon = '$svgBasePath/radiology_icon.svg'; static const String prescription_item_icon = '$svgBasePath/prescription_item_icon.svg'; + static const String vital_sign_item_icon = '$svgBasePath/vital_sign_item_icon.svg'; static const String checkin_location_icon = '$svgBasePath/checkin_location_icon.svg'; static const String checkin_nfc_icon = '$svgBasePath/checkin_nfc_icon.svg'; static const String checkin_qr_icon = '$svgBasePath/checkin_qr_icon.svg'; diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index 8f439d2c..6e01a561 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -67,7 +67,7 @@ abstract class HmgServicesRepo { Future>> getCovidPaymentInfo(String procedureID, int projectID); - Future>>> getPatientVitalSign(); + Future>>> getPatientVitalSign( num? transID, int? projectID); } class HmgServicesRepoImp implements HmgServicesRepo { @@ -917,8 +917,11 @@ class HmgServicesRepoImp implements HmgServicesRepo { } @override - Future>>> getPatientVitalSign() async { + Future>>> getPatientVitalSign(num? transID, int? projectID) async { Map requestBody = { + "TransID": transID, + "TransNo": transID, + "ProjectID": projectID, }; try { diff --git a/lib/features/hmg_services/hmg_services_view_model.dart b/lib/features/hmg_services/hmg_services_view_model.dart index da1ae80d..f462aedd 100644 --- a/lib/features/hmg_services/hmg_services_view_model.dart +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -895,12 +895,23 @@ class HmgServicesViewModel extends ChangeNotifier { Future getPatientVitalSign({ Function(dynamic)? onSuccess, Function(String)? onError, + num? transNo, + int? projectID, + bool forceRefresh = false, }) async { - if (hasVitalSignDataLoaded) return; + if (hasVitalSignDataLoaded && transNo == null && projectID == null && !forceRefresh) { + if (onSuccess != null) { + onSuccess(GenericApiModel>( + messageStatus: 1, + data: vitalSignList, + )); + } + return; + } isVitalSignLoading = true; notifyListeners(); - final result = await hmgServicesRepo.getPatientVitalSign(); + final result = await hmgServicesRepo.getPatientVitalSign(transNo, projectID); result.fold( (failure) async { @@ -915,7 +926,9 @@ class HmgServicesViewModel extends ChangeNotifier { isVitalSignLoading = false; if (apiResponse.messageStatus == 1) { vitalSignList = apiResponse.data ?? []; - hasVitalSignDataLoaded = true; + if (transNo == null && projectID == null) { + hasVitalSignDataLoaded = true; + } if (vitalSignList.isNotEmpty) { getIt.get().setSelectedHeight(vitalSignList.first.heightCm); getIt.get().setSelectedWeight(vitalSignList.first.weightKg); diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 4d181230..5ba715e9 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -22,6 +22,9 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; import 'package:hmg_patient_app_new/features/hospital/AppPermission.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; @@ -40,6 +43,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appoin import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/bell_switch.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/clarity_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -666,6 +670,76 @@ class _AppointmentDetailsPageState extends State { padding: EdgeInsets.zero, shrinkWrap: true, children: [ + MedicalFileCard( + label: LocaleKeys.vitalSigns.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.vital_sign_item_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + final hmgServicesVM = getIt.get(); + final appt = widget.patientAppointmentHistoryResponseModel; + final targetTransNo = appt.vitalSignAppointmentNo ?? appt.appointmentNo; + + await hmgServicesVM.getPatientVitalSign( + transNo: targetTransNo, + projectID: appt.projectID, + onSuccess: (apiResponse) { + LoaderBottomSheet.hideLoader(); + final List vitalList = (apiResponse is GenericApiModel>) + ? (apiResponse.data ?? []) + : (apiResponse.data is List + ? (apiResponse.data as List) + : hmgServicesVM.vitalSignList); + + VitalSignResModel? matchedVitalSign; + if (vitalList.isNotEmpty) { + // 1. Match by vitalSignAppointmentNo or appointmentNo + if (targetTransNo != null) { + final targetStr = targetTransNo.toString(); + for (final v in vitalList) { + if (v.transNo != null && v.transNo.toString() == targetStr) { + matchedVitalSign = v; + break; + } + } + } + + // 2. If specific API returned records for this TransNo/ProjectID, fallback to first + matchedVitalSign ??= vitalList.first; + + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage( + specificVitalSign: matchedVitalSign, + isFromAppointment: true, + ), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.noVitalSignsRecordedYet.tr(context: context)), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + }), MedicalFileCard( label: LocaleKeys.labResults.tr(context: context), textColor: AppColors.blackColor, @@ -790,6 +864,7 @@ class _AppointmentDetailsPageState extends State { // ), // ); }), + MedicalFileCard( label: LocaleKeys.doctorResponses.tr(context: context), textColor: AppColors.blackColor, @@ -945,6 +1020,7 @@ class _AppointmentDetailsPageState extends State { ); }) : SizedBox.shrink(), + ], ); }), diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index 63b21291..48532645 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -53,8 +53,15 @@ class VitalSignDetailsArgs { class VitalSignDetailsPage extends StatefulWidget { final VitalSignDetailsArgs args; - - const VitalSignDetailsPage({super.key, required this.args}); + final VitalSignResModel? selectedVitalSign; + final bool isFromAppointment; + + const VitalSignDetailsPage({ + super.key, + required this.args, + this.selectedVitalSign, + this.isFromAppointment = false, + }); @override State createState() => _VitalSignDetailsPageState(); @@ -65,17 +72,28 @@ class _VitalSignDetailsPageState extends State { VitalSignDetailsArgs get args => widget.args; + @override + void initState() { + super.initState(); + if (widget.isFromAppointment) { + WidgetsBinding.instance.addPostFrameCallback((_) { + getIt.get().getPatientVitalSign(forceRefresh: true); + }); + } + } + @override Widget build(BuildContext context) { return CollapsingListView( title: LocaleKeys.vitalSignDetails.tr(context: context), child: Consumer( builder: (context, viewModel, child) { - final latest = viewModel.vitalSignList.isNotEmpty ? viewModel.vitalSignList.first : null; + final targetVitalSign = widget.selectedVitalSign ?? + (viewModel.vitalSignList.isNotEmpty ? viewModel.vitalSignList.first : null); final history = _buildSeries(viewModel.vitalSignList, args); - final latestValueText = _latestValueText(latest); - final status = _statusForLatest(latest); + final latestValueText = _latestValueText(targetVitalSign); + final status = _statusForLatest(targetVitalSign); final scheme = VitalSignUiModel.scheme(status: status, label: args.title); return SingleChildScrollView( @@ -89,7 +107,7 @@ class _VitalSignDetailsPageState extends State { valueText: latestValueText, status: status, scheme: scheme, - latestDate: latest?.vitalSignDate, + latestDate: targetVitalSign?.vitalSignDate, ), _whatIsThisResultCard(context), _historyCard(context, history: history), @@ -531,6 +549,17 @@ class _VitalSignDetailsPageState extends State { return RangeAnnotations(horizontalRangeAnnotations: ranges); } + String _formatValue(dynamic value) { + if (value == null) return '--'; + if (value is num && value == 0) return '--'; + if (value is String) { + if (value.trim().isEmpty) return '--'; + final parsed = double.tryParse(value); + if (parsed == null || parsed == 0) return '--'; + } + return value.toString(); + } + List _buildSeries(List vitals, VitalSignDetailsArgs args) { final List points = []; @@ -578,7 +607,7 @@ class _VitalSignDetailsPageState extends State { DataPoint( value: index, label: label, - actualValue: mv.toStringAsFixed(0), + actualValue: _formatValue(mv), time: dt, displayTime: '${dt.day}/${dt.month}/${dt.year}', unitOfMeasurement: args.unit, @@ -618,7 +647,7 @@ class _VitalSignDetailsPageState extends State { DataPoint( value: index, label: label, - actualValue: diastolic.toStringAsFixed(0), + actualValue: _formatValue(diastolic), time: dt, displayTime: '${dt.day}/${dt.month}/${dt.year}', unitOfMeasurement: args.unit, @@ -646,28 +675,27 @@ class _VitalSignDetailsPageState extends State { switch (args.metric) { case VitalSignMetric.bmi: - final v = _toDouble(latest.bodyMassIndex); - return v == null ? '--' : v.toStringAsFixed(0); + return _formatValue(latest.bodyMassIndex); case VitalSignMetric.height: - final v = _toDouble(latest.heightCm); - return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + final formatted = _formatValue(latest.heightCm); + return formatted == '--' ? '--' : (args.unit.isNotEmpty ? '$formatted ${args.unit}' : formatted); case VitalSignMetric.weight: - final v = _toDouble(latest.weightKg); - return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + final formatted = _formatValue(latest.weightKg); + return formatted == '--' ? '--' : (args.unit.isNotEmpty ? '$formatted ${args.unit}' : formatted); case VitalSignMetric.temperature: - final v = _toDouble(latest.temperatureCelcius); - return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + final formatted = _formatValue(latest.temperatureCelcius); + return formatted == '--' ? '--' : (args.unit.isNotEmpty ? '$formatted ${args.unit}' : formatted); case VitalSignMetric.heartRate: - final v = _toDouble(latest.heartRate ?? latest.pulseBeatPerMinute); - return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + final formatted = _formatValue(latest.heartRate ?? latest.pulseBeatPerMinute); + return formatted == '--' ? '--' : (args.unit.isNotEmpty ? '$formatted ${args.unit}' : formatted); case VitalSignMetric.respiratoryRate: - final v = _toDouble(latest.respirationBeatPerMinute); - return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + final formatted = _formatValue(latest.respirationBeatPerMinute); + return formatted == '--' ? '--' : (args.unit.isNotEmpty ? '$formatted ${args.unit}' : formatted); case VitalSignMetric.bloodPressure: - final s = _toDouble(latest.bloodPressureHigher); - final d = _toDouble(latest.bloodPressureLower); - if (s == null || d == null) return '--'; - return '${s.toStringAsFixed(0)}/${d.toStringAsFixed(0)}'; + final s = _formatValue(latest.bloodPressureHigher); + final d = _formatValue(latest.bloodPressureLower); + if (s == '--' || d == '--') return '--'; + return '$s/$d'; } } diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index 826c3971..2be990f7 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -21,7 +21,14 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class VitalSignPage extends StatefulWidget { - const VitalSignPage({super.key}); + final VitalSignResModel? specificVitalSign; + final bool isFromAppointment; + + const VitalSignPage({ + super.key, + this.specificVitalSign, + this.isFromAppointment = false, + }); @override State createState() => _VitalSignPageState(); @@ -53,10 +60,14 @@ class _VitalSignPageState extends State { return value.toString(); } - void _openDetails(VitalSignDetailsArgs args) { + void _openDetails(VitalSignDetailsArgs args, VitalSignResModel? targetVitalSign) { Navigator.of(context).push( CustomPageRoute( - page: VitalSignDetailsPage(args: args), + page: VitalSignDetailsPage( + args: args, + selectedVitalSign: targetVitalSign, + isFromAppointment: widget.isFromAppointment, + ), ), ); } @@ -75,20 +86,21 @@ class _VitalSignPageState extends State { child: Consumer( builder: (context, viewModel, child) { - // Get the latest vital sign data (first item in the list) - VitalSignResModel? latestVitalSign = viewModel.vitalSignList.isNotEmpty - ? viewModel.vitalSignList.first - : null; + // Get target vital sign: specificVitalSign if passed, otherwise first in list + VitalSignResModel? targetVitalSign = widget.specificVitalSign ?? + (viewModel.vitalSignList.isNotEmpty + ? viewModel.vitalSignList.first + : null); // Debug logging for blood pressure - // if (latestVitalSign != null) { + // if (targetVitalSign != null) { // print('=== Blood Pressure Debug ==='); - // print('bloodPressureHigher: ${latestVitalSign.bloodPressureHigher}'); - // print('bloodPressureLower: ${latestVitalSign.bloodPressureLower}'); - // print('bloodPressureHigher type: ${latestVitalSign.bloodPressureHigher.runtimeType}'); - // print('bloodPressureLower type: ${latestVitalSign.bloodPressureLower.runtimeType}'); - // print('bloodPressureHigher == 0: ${latestVitalSign.bloodPressureHigher == 0}'); - // print('bloodPressureLower == 0: ${latestVitalSign.bloodPressureLower == 0}'); + // print('bloodPressureHigher: ${targetVitalSign.bloodPressureHigher}'); + // print('bloodPressureLower: ${targetVitalSign.bloodPressureLower}'); + // print('bloodPressureHigher type: ${targetVitalSign.bloodPressureHigher.runtimeType}'); + // print('bloodPressureLower type: ${targetVitalSign.bloodPressureLower.runtimeType}'); + // print('bloodPressureHigher == 0: ${targetVitalSign.bloodPressureHigher == 0}'); + // print('bloodPressureLower == 0: ${targetVitalSign.bloodPressureLower == 0}'); // print('========================'); // } @@ -112,9 +124,9 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.bmiVital, label: LocaleKeys.bmi.tr(context: context), - value: _formatValue(latestVitalSign?.bodyMassIndex), + value: _formatValue(targetVitalSign?.bodyMassIndex), unit: '', - status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), + status: VitalSignUiModel.bmiStatus(targetVitalSign?.bodyMassIndex), onTap: () => _openDetails( VitalSignDetailsArgs( metric: VitalSignMetric.bmi, @@ -122,6 +134,7 @@ class _VitalSignPageState extends State { icon: AppAssets.bmiVital, unit: '', ), + targetVitalSign, ), ), SizedBox(height: 16.h), @@ -130,7 +143,7 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.heightVital, label: LocaleKeys.height.tr(context: context), - value: _formatValue(latestVitalSign?.heightCm), + value: _formatValue(targetVitalSign?.heightCm), unit: 'cm', status: null, onTap: () => _openDetails( @@ -140,6 +153,7 @@ class _VitalSignPageState extends State { icon: AppAssets.heightVital, unit: 'cm', ), + targetVitalSign, ), ), SizedBox(height: 16.h), @@ -148,10 +162,10 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.weightVital, label: LocaleKeys.weight.tr(context: context), - value: _formatValue(latestVitalSign?.weightKg), + value: _formatValue(targetVitalSign?.weightKg), unit: 'kg', - status: (latestVitalSign?.weightKg != null) - ? VitalSignUiModel.weightStatus(latestVitalSign?.bodyMassIndex) + status: (targetVitalSign?.weightKg != null) + ? VitalSignUiModel.weightStatus(targetVitalSign?.bodyMassIndex) : null, onTap: () => _openDetails( VitalSignDetailsArgs( @@ -160,6 +174,7 @@ class _VitalSignPageState extends State { icon: AppAssets.weightVital, unit: 'kg', ), + targetVitalSign, ), ), SizedBox(height: 16.h), @@ -168,18 +183,18 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.bloodPressure, label: LocaleKeys.bloodPressure.tr(context: context), - value: (latestVitalSign != null && - _isValidValue(latestVitalSign.bloodPressureHigher) && - _isValidValue(latestVitalSign.bloodPressureLower)) - ? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}' + value: (targetVitalSign != null && + _isValidValue(targetVitalSign.bloodPressureHigher) && + _isValidValue(targetVitalSign.bloodPressureLower)) + ? '${targetVitalSign.bloodPressureHigher}/${targetVitalSign.bloodPressureLower}' : '--', unit: '', - status: (latestVitalSign != null && - _isValidValue(latestVitalSign.bloodPressureHigher) && - _isValidValue(latestVitalSign.bloodPressureLower)) + status: (targetVitalSign != null && + _isValidValue(targetVitalSign.bloodPressureHigher) && + _isValidValue(targetVitalSign.bloodPressureLower)) ? VitalSignUiModel.bloodPressureStatus( - systolic: latestVitalSign.bloodPressureHigher, - diastolic: latestVitalSign.bloodPressureLower, + systolic: targetVitalSign.bloodPressureHigher, + diastolic: targetVitalSign.bloodPressureLower, ) : null, onTap: () => _openDetails( @@ -191,6 +206,7 @@ class _VitalSignPageState extends State { low: 90, high: 140, ), + targetVitalSign, ), ), SizedBox(height: 16.h), @@ -199,7 +215,7 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.temperature, label: LocaleKeys.temperature.tr(context: context), - value: _formatValue(latestVitalSign?.temperatureCelcius), + value: _formatValue(targetVitalSign?.temperatureCelcius), unit: '°C', status: null, onTap: () => _openDetails( @@ -211,6 +227,7 @@ class _VitalSignPageState extends State { low: 36.1, high: 37.2, ), + targetVitalSign, ), ), ], @@ -275,9 +292,9 @@ class _VitalSignPageState extends State { child: _buildVitalSignCard( icon: AppAssets.heart, label: LocaleKeys.heart.tr(context: context), - value: _formatValue(latestVitalSign?.heartRate ?? latestVitalSign?.pulseBeatPerMinute), + value: _formatValue(targetVitalSign?.heartRate ?? targetVitalSign?.pulseBeatPerMinute), unit: 'bpm', - status: _isValidValue(latestVitalSign?.heartRate ?? latestVitalSign?.pulseBeatPerMinute) ? VitalSignStatus.normal : null, + status: _isValidValue(targetVitalSign?.heartRate ?? targetVitalSign?.pulseBeatPerMinute) ? VitalSignStatus.normal : null, onTap: () => _openDetails( VitalSignDetailsArgs( metric: VitalSignMetric.heartRate, @@ -287,6 +304,7 @@ class _VitalSignPageState extends State { low: 60, high: 100, ), + targetVitalSign, ), ), ), @@ -299,9 +317,9 @@ class _VitalSignPageState extends State { _buildVitalSignCard( icon: AppAssets.respRate, label: LocaleKeys.respirationRate.tr(context: context), - value: _formatValue(latestVitalSign?.respirationBeatPerMinute), + value: _formatValue(targetVitalSign?.respirationBeatPerMinute), unit: 'bpm', - status: _isValidValue(latestVitalSign?.respirationBeatPerMinute) ? VitalSignStatus.normal : null, + status: _isValidValue(targetVitalSign?.respirationBeatPerMinute) ? VitalSignStatus.normal : null, onTap: () => _openDetails( VitalSignDetailsArgs( metric: VitalSignMetric.respiratoryRate, @@ -311,6 +329,7 @@ class _VitalSignPageState extends State { low: 12, high: 20, ), + targetVitalSign, ), ), ], From b32c15a88693c1426949ebf989b730b16977f6eb Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 1 Sep 2026 09:47:19 +0300 Subject: [PATCH 49/73] updates --- lib/features/paytabs/paytabs_view_model.dart | 5 +++++ .../appointments/widgets/appointment_card.dart | 1 + .../livecare/immediate_livecare_payment_page.dart | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index a7894f07..35d3c85b 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -52,6 +52,11 @@ class PayTabsViewModel extends ChangeNotifier { profileId: paytabsProjectDetailsRespModel!.merchantIdentifier, serverKey: paytabsProjectDetailsRespModel!.accessCode, clientKey: paytabsProjectDetailsRespModel!.shaResponse, + + // profileId: PaymentSdkDefaultConfig.profileID, + // serverKey: PaymentSdkDefaultConfig.serverKey, + // clientKey: PaymentSdkDefaultConfig.clientKey, + transactionType: PaymentSdkTransactionType.SALE, cartId: cartID.substring((cartID.length - 5), cartID.length), cartDescription: paymentDescription, diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 1cbe3be2..38c6c7d3 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -637,6 +637,7 @@ class _AppointmentCardState extends State { } void _goToDetails(BuildContext context) { + if (widget.isLoading) return; if (widget.isFromMedicalReport) return; if (widget.isForEyeMeasurements) { Navigator.of(context).push( 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 692e8b44..96e38363 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -817,7 +817,13 @@ class _ImmediateLiveCarePaymentPageState extends State Date: Tue, 1 Sep 2026 09:55:00 +0300 Subject: [PATCH 50/73] no message --- assets/images/svg/vital_sign_item_icon.svg | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 assets/images/svg/vital_sign_item_icon.svg diff --git a/assets/images/svg/vital_sign_item_icon.svg b/assets/images/svg/vital_sign_item_icon.svg new file mode 100644 index 00000000..3ebe220a --- /dev/null +++ b/assets/images/svg/vital_sign_item_icon.svg @@ -0,0 +1,7 @@ + + + + + + + From 81a07e4d3f2003ece98ceb7b729de8c725df14f7 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Tue, 1 Sep 2026 11:22:29 +0300 Subject: [PATCH 51/73] vital sign bug fix --- .../hmg_services/hmg_services_view_model.dart | 37 ++++++++++++------- .../appointment_details_page.dart | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/features/hmg_services/hmg_services_view_model.dart b/lib/features/hmg_services/hmg_services_view_model.dart index f462aedd..9cd7e519 100644 --- a/lib/features/hmg_services/hmg_services_view_model.dart +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -899,7 +899,9 @@ class HmgServicesViewModel extends ChangeNotifier { int? projectID, bool forceRefresh = false, }) async { - if (hasVitalSignDataLoaded && transNo == null && projectID == null && !forceRefresh) { + final bool isSpecificQuery = (transNo != null || projectID != null); + + if (!isSpecificQuery && hasVitalSignDataLoaded && !forceRefresh) { if (onSuccess != null) { onSuccess(GenericApiModel>( messageStatus: 1, @@ -908,37 +910,44 @@ class HmgServicesViewModel extends ChangeNotifier { } return; } - isVitalSignLoading = true; - notifyListeners(); + + if (!isSpecificQuery) { + isVitalSignLoading = true; + notifyListeners(); + } final result = await hmgServicesRepo.getPatientVitalSign(transNo, projectID); result.fold( (failure) async { - isVitalSignLoading = false; - notifyListeners(); + if (!isSpecificQuery) { + isVitalSignLoading = false; + notifyListeners(); + } await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.message); } }, (apiResponse) { - isVitalSignLoading = false; - if (apiResponse.messageStatus == 1) { - vitalSignList = apiResponse.data ?? []; - if (transNo == null && projectID == null) { + if (!isSpecificQuery) { + isVitalSignLoading = false; + if (apiResponse.messageStatus == 1) { + vitalSignList = apiResponse.data ?? []; hasVitalSignDataLoaded = true; - } - if (vitalSignList.isNotEmpty) { - getIt.get().setSelectedHeight(vitalSignList.first.heightCm); - getIt.get().setSelectedWeight(vitalSignList.first.weightKg); + if (vitalSignList.isNotEmpty) { + getIt.get().setSelectedHeight(vitalSignList.first.heightCm); + getIt.get().setSelectedWeight(vitalSignList.first.weightKg); + } } notifyListeners(); + } + + if (apiResponse.messageStatus == 1) { if (onSuccess != null) { onSuccess(apiResponse); } } else { - notifyListeners(); if (onError != null) { onError(apiResponse.errorMessage ?? 'Unknown error'); } diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index e384793b..dc147981 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -692,7 +692,7 @@ class _AppointmentDetailsPageState extends State { ? (apiResponse.data ?? []) : (apiResponse.data is List ? (apiResponse.data as List) - : hmgServicesVM.vitalSignList); + : []); VitalSignResModel? matchedVitalSign; if (vitalList.isNotEmpty) { From de75c6adc1d2a40dabc8a6a0f8c5145e7f74236e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 1 Sep 2026 14:13:00 +0300 Subject: [PATCH 52/73] Updates --- .../authentication/authentication_view_model.dart | 4 ++++ .../medical_file/medical_file_view_model.dart | 10 ++++++++++ .../prescriptions/prescriptions_view_model.dart | 5 +++++ .../livecare/immediate_livecare_payment_page.dart | 15 ++++++++++++--- .../medical_file/medical_file_page.dart | 2 -- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 72ca42cb..a8719f29 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; @@ -1280,6 +1281,9 @@ class AuthenticationViewModel extends ChangeNotifier { getIt.get().setIsWalletAmountToBeLoaded(true); getIt.get().setIsInsuranceDataToBeLoaded(true); getIt.get().setHasVitalSignDataLoaded(false); + getIt.get().setIsSickLeavesDataNeedsReloading(true); + getIt.get().setIsMedicalReportsDataNeedsReloading(true); + getIt.get().setIsPrescriptionDataNeedsReloading(true); if (isUserAgreedBefore) { LoaderBottomSheet.hideLoader(); navigateToHomeScreen(); diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index a9bfb09c..29f653ec 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -168,6 +168,16 @@ class MedicalFileViewModel extends ChangeNotifier { notifyListeners(); } + setIsSickLeavesDataNeedsReloading(bool value) { + isSickLeavesDataNeedsReloading = value; + notifyListeners(); + } + + setIsMedicalReportsDataNeedsReloading(bool value) { + isMedicalReportsDataNeedsReloading = value; + notifyListeners(); + } + setIsPatientSickLeaveListLoading(bool val) { if (val && isSickLeavesDataNeedsReloading) { patientSickLeaveList.clear(); diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index d75b3f8e..1e33b509 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -78,6 +78,11 @@ class PrescriptionsViewModel extends ChangeNotifier { notifyListeners(); } + void setIsPrescriptionDataNeedsReloading(bool value) { + isPrescriptionsDataNeedsReloading = value; + notifyListeners(); + } + Future checkIfReminderExistForPrescription(int index) async { final item = prescriptionDetailsList[index]; // Check device calendar 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 96e38363..31defa56 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -819,14 +819,23 @@ class _ImmediateLiveCarePaymentPageState extends State { 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(); From 73bc66f9c9c92fd706f2dcdb63d2fe2c9f099a54 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 10 Sep 2026 11:02:26 +0300 Subject: [PATCH 53/73] CMC image view issue. --- lib/core/utils/utils.dart | 4 ++-- .../comprehensive_checkup_page.dart | 15 +++++++++------ .../media_viewer/full_screen_image_viewer.dart | 18 ++++++++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index fc81663e..e5761203 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -808,8 +808,8 @@ class Utils { double? border, double? borderRadius, }) { - final iconH = height ?? 24.h; - final iconW = width ?? 24.w; + final double? iconW = (width == null && height == null) ? 24.w : width; + final double? iconH = (width == null && height == null) ? 24.h : height; return PreserveImageColors( child: Container( decoration: BoxDecoration( diff --git a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart index 7e7bc613..88199055 100644 --- a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart +++ b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart @@ -282,19 +282,22 @@ class _ComprehensiveCheckupPageState extends State { final String imagePath = appStateLocal.isArabic() ? AppAssets.comprehensiveCheckupAr : AppAssets.comprehensiveCheckupEn; return Stack( children: [ - Utils.buildImgWithAssets( - icon: imagePath, - width: double.infinity, - fit: BoxFit.cover, + ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: Utils.buildImgWithAssets( + icon: imagePath, + width: double.infinity, + fit: BoxFit.fitWidth, + ), ).paddingAll(16.w), Align( alignment: Alignment.topRight, child: Container( decoration: BoxDecoration( - color: Color.fromARGB(51, 0, 0, 0), + color: Color.fromARGB(120, 0, 0, 0), borderRadius: BorderRadius.circular(1000.r), ), - margin: EdgeInsets.all(16.h), + margin: EdgeInsets.all(24.w), child: IconButton( icon: Icon( Icons.zoom_in, diff --git a/lib/widgets/media_viewer/full_screen_image_viewer.dart b/lib/widgets/media_viewer/full_screen_image_viewer.dart index ae6d4802..ce766204 100644 --- a/lib/widgets/media_viewer/full_screen_image_viewer.dart +++ b/lib/widgets/media_viewer/full_screen_image_viewer.dart @@ -13,17 +13,27 @@ class FullScreenImageViewer extends StatelessWidget { return Scaffold( appBar: AppBar( backgroundColor: Colors.black, + elevation: 0, leading: IconButton( icon: Icon(Icons.close, color: AppColors.whiteColor), onPressed: () => Navigator.of(context).pop(), ), ), backgroundColor: Colors.black, - body: Center( + body: SizedBox.expand( child: InteractiveViewer( - child: isSvg - ? Utils.buildSvgWithAssets(icon: path, width: double.infinity, fit: BoxFit.contain, applyThemeColor: false) - : Utils.buildImgWithAssets(icon: path, width: double.infinity, fit: BoxFit.contain), + minScale: 0.5, + maxScale: 6.0, + boundaryMargin: const EdgeInsets.all(20), + child: Center( + child: isSvg + ? Utils.buildSvgWithAssets(icon: path, fit: BoxFit.contain, applyThemeColor: false) + : Image.asset( + path, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => Utils.buildImgWithAssets(icon: path, fit: BoxFit.contain), + ), + ), ), ), ); From 1da9cac4bfa235f908ade20cee044de03a2d55f5 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 10 Sep 2026 12:25:26 +0300 Subject: [PATCH 54/73] Medical report date fix --- lib/presentation/medical_report/medical_reports_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index 9463debe..e1ac3969 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -260,7 +260,7 @@ class _MedicalReportsPageState extends State { runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false), + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.encounterDate), false), ), AppCustomChipWidget( labelText: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!, From ce41d7c1f0331963829f6dfb98b6b95818a9cade Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 10 Sep 2026 14:44:19 +0300 Subject: [PATCH 55/73] CMC summary page updated --- lib/core/utils/utils.dart | 10 +++--- lib/widgets/map/location_map_widget.dart | 41 ++++++++++++------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index e5761203..d3107d72 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -859,15 +859,15 @@ class Utils { BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder, }) { + final double? iconW = (width == null && height == null) ? 24.w : width; + final double? iconH = (width == null && height == null) ? 24.h : height; if (url == null || url.isEmpty) { return Utils.buildSvgWithAssets( - width: width ?? 24.w, - height: height ?? 24.h, + width: iconW ?? 24.w, + height: iconH ?? 24.h, icon: AppAssets.noImageFound, ); } - final iconH = height ?? 24.h; - final iconW = width ?? 24.w; return PreserveImageColors( child: Image.network( url, @@ -877,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.noImageFound); + return Utils.buildSvgWithAssets(width: iconW ?? 24.w, height: iconH ?? 24.h, icon: AppAssets.noImageFound); }, ), ); diff --git a/lib/widgets/map/location_map_widget.dart b/lib/widgets/map/location_map_widget.dart index f3b57aa3..044b3b76 100644 --- a/lib/widgets/map/location_map_widget.dart +++ b/lib/widgets/map/location_map_widget.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -131,11 +130,14 @@ class LocationMapWidget extends StatelessWidget { borderRadius: BorderRadius.circular(24.r), child: Utils.buildImgWithNetwork( url: staticMapUrl, + width: double.infinity, + height: 165.h, iconColor: AppColors.transparent, - fit: BoxFit.contain, + fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( height: 165.h, + width: double.infinity, decoration: BoxDecoration( color: AppColors.bgScaffoldColor, borderRadius: BorderRadius.circular(24.r), @@ -153,24 +155,23 @@ class LocationMapWidget extends StatelessWidget { ), Positioned( bottom: 0, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.785, - child: CustomButton( - text: LocaleKeys.getDirections.tr(context: context), - onPressed: onDirectionsTap ?? _defaultLaunchDirections, - backgroundColor: AppColors.textColor.withValues(alpha: 0.8), - borderColor: AppColors.textColor.withValues(alpha: 0.01), - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - icon: AppAssets.directions_icon, - iconColor: AppColors.whiteColor, - iconSize: 14.h, - ).paddingAll(12.h), - ), + left: 0, + right: 0, + child: CustomButton( + text: LocaleKeys.getDirections.tr(context: context), + onPressed: onDirectionsTap ?? _defaultLaunchDirections, + backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + borderColor: AppColors.textColor.withValues(alpha: 0.01), + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppAssets.directions_icon, + iconColor: AppColors.whiteColor, + iconSize: 14.h, + ).paddingAll(12.h), ), ], ), From c4cf67119c9b020881b8bd149ae68d524f3b78f0 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 13 Sep 2026 16:56:59 +0300 Subject: [PATCH 56/73] Dubai timezone handled --- lib/core/utils/date_util.dart | 20 +++++++++---------- .../widgets/appointment_card.dart | 2 +- .../widgets/appointment_doctor_card.dart | 5 +---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index 97c8dad0..7510786d 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -28,21 +28,21 @@ class DateUtil { const end = "+0300)"; final startIndex = date.indexOf(start); final endIndex = date.indexOf(end, startIndex + start.length); - // if (projectId == 2 || projectId == 3) { - // return DateTime.fromMillisecondsSinceEpoch( - // int.parse( - // date.substring(startIndex + start.length, endIndex), - // ), - // isUtc: true) - // .add(Duration(hours: 4)); - // } else { - return DateTime.fromMillisecondsSinceEpoch( + if (projectId == 2 || projectId == 3) { + return DateTime.fromMillisecondsSinceEpoch( + int.parse( + date.substring(startIndex + start.length, endIndex), + ), + isUtc: true) + .add(Duration(hours: 4)); + } else { + return DateTime.fromMillisecondsSinceEpoch( int.parse( date.substring(startIndex + start.length, endIndex), ), isUtc: true) .add(Duration(hours: 3)); - // } + } } else { return DateTime.now(); } diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 38c6c7d3..1eeb842c 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -321,7 +321,7 @@ class _AppointmentCardState extends State { icon: AppAssets.appointment_calendar_icon, richText: widget.isLoading ? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading) - : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}" + : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(widget.patientAppointmentHistoryResponseModel.startTime != null && widget.patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? widget.patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(widget.patientAppointmentHistoryResponseModel.appointmentDate!, widget.patientAppointmentHistoryResponseModel.projectID), false)}" .toText10(isEnglishOnly: true, isBold: true), ).toShimmer2(isShow: widget.isLoading), ), diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index dc7bb0af..4dd838ea 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -142,10 +142,7 @@ class AppointmentDoctorCard extends StatelessWidget { child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - richText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang( - DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), - false, - )}" + richText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(patientAppointmentHistoryResponseModel.startTime != null && patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(patientAppointmentHistoryResponseModel.appointmentDate!, patientAppointmentHistoryResponseModel.projectID), false)}" .toText10(isBold: true), ), ), From 621cbdb165e2b6655ccd2c814614f1f26c4ebb08 Mon Sep 17 00:00:00 2001 From: Haroon6138 Date: Mon, 14 Sep 2026 06:59:20 +0000 Subject: [PATCH 57/73] Habib Wallet & Lakum card updates --- .../habib_wallet/habib_wallet_view_model.dart | 34 ++- .../appointment_payment_page.dart | 2 +- .../waiting_appointment_payment_page.dart | 2 +- .../er_online_checkin_payment_page.dart | 248 +++++++++--------- lib/presentation/home/landing_page.dart | 16 +- .../home/widgets/habib_wallet_card.dart | 39 ++- .../home/widgets/lakum_wallet_card.dart | 49 ++-- lib/widgets/in_app_browser/InAppBrowser.dart | 2 +- 8 files changed, 203 insertions(+), 189 deletions(-) diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index cc87bb7b..e888600a 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -30,7 +30,35 @@ class HabibWalletViewModel extends ChangeNotifier { num habibWalletAmount = 0; num walletRechargeAmount = 0; String notesText = ""; - bool isWalletAmountToBeLoaded = true; + bool isWalletAmountToBeLoaded = false; + + // ── Balance visibility (show/hide like a password field) ────────────────── + bool isWalletAmountHidden = true; + bool isLakumPointsHidden = true; + + // Track whether data has already been fetched (to avoid re-calling APIs). + bool isWalletAmountFetched = false; + bool isLakumInfoFetched = false; + + void toggleWalletAmountVisibility() { + isWalletAmountHidden = !isWalletAmountHidden; + notifyListeners(); + // On first unhide, fetch the data if not already fetched. + if (!isWalletAmountHidden && !isWalletAmountFetched) { + isWalletAmountToBeLoaded = true; + getPatientBalanceAmount(); + } + } + + void toggleLakumPointsVisibility() { + isLakumPointsHidden = !isLakumPointsHidden; + notifyListeners(); + // On first unhide, fetch the data if not already fetched. + if (!isLakumPointsHidden && !isLakumInfoFetched) { + isWalletAmountToBeLoaded = true; + getLakumAccountInformation(); + } + } bool isBottomSheetContentLoading = false; @@ -70,6 +98,8 @@ class HabibWalletViewModel extends ChangeNotifier { bool isLakumAccountInfoLoading = false; LakumInquiryInformationResponseModel? lakumAccountInfo; + + // Get all flattened transactions sorted by date descending List getAllLakumTransactions() { if (lakumAccountInfo?.gainedPointsAmountPerYear == null) { @@ -437,6 +467,7 @@ class HabibWalletViewModel extends ChangeNotifier { isWalletAmountLoading = false; isWalletAmountToBeLoaded = false; + isWalletAmountFetched = true; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -581,6 +612,7 @@ class HabibWalletViewModel extends ChangeNotifier { ); } isLakumAccountInfoLoading = false; + isLakumInfoFetched = true; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 85a606b3..ffa9b723 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -726,7 +726,7 @@ class _AppointmentPaymentPageState extends State { ); }); } else { - openPaymentURL("tamara"); + openPaymentURL("TAMARA"); } }) : SizedBox.shrink(), 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 82ea1578..970bfb4e 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 @@ -471,7 +471,7 @@ class _WaitingAppointmentPaymentPageState extends State emergencyServicesViewModel.selectedHospital!.iD, appState.getAuthenticatedUser()!.patientId!, ); - paytabsViewModel.setPaymentConfiguration( - "ER Online Check-In Payment", - emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, + paytabsViewModel.setPaymentConfiguration("ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, @@ -147,45 +145,46 @@ class _ErOnlineCheckinPaymentPageState extends State projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(), onSuccess: (PaytabsTransactionResponseModel transactionData) async { if (emergencyServicesViewModel.isERBookAppointment) { - await emergencyServicesViewModel.ER_CreateAdvancePayment( - paymentMethodName: selectedPaymentMethod, - paymentReference: transactionData.transactionReference!, - onSuccess: (value) async { - await emergencyServicesViewModel.addAdvanceNumberRequest( - advanceNumber: value, - paymentReference: transactionData.transactionReference!, - appointmentNo: "0", - onSuccess: (val) { - emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true); - LoaderBottomSheet.hideLoader(); - if (emergencyServicesViewModel.isERBookAppointment) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)), - callBackFunc: () { - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } else {} - }); - }); - } else {} - }, onError: (errMsg) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: errMsg.toString()), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); + await emergencyServicesViewModel.ER_CreateAdvancePayment( + paymentMethodName: selectedPaymentMethod, + paymentReference: transactionData.transactionReference!, + onSuccess: (value) async { + await emergencyServicesViewModel.addAdvanceNumberRequest( + advanceNumber: value, + paymentReference: transactionData.transactionReference!, + appointmentNo: "0", + onSuccess: (val) { + emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true); + LoaderBottomSheet.hideLoader(); + if (emergencyServicesViewModel.isERBookAppointment) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)), + callBackFunc: () { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else {} + }); + }); + } else {} + }, + onError: (errMsg) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: errMsg.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); } else { openPaymentURL("mada"); } @@ -235,9 +234,7 @@ class _ErOnlineCheckinPaymentPageState extends State emergencyServicesViewModel.selectedHospital!.iD, appState.getAuthenticatedUser()!.patientId!, ); - paytabsViewModel.setPaymentConfiguration( - "ER Online Check-In Payment", - emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, + paytabsViewModel.setPaymentConfiguration("ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startCardPayment( paymentOption: selectedPaymentMethod, @@ -254,45 +251,46 @@ class _ErOnlineCheckinPaymentPageState extends State projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(), onSuccess: (PaytabsTransactionResponseModel transactionData) async { if (emergencyServicesViewModel.isERBookAppointment) { - await emergencyServicesViewModel.ER_CreateAdvancePayment( - paymentMethodName: selectedPaymentMethod, - paymentReference: transactionData.transactionReference!, - onSuccess: (value) async { - await emergencyServicesViewModel.addAdvanceNumberRequest( - advanceNumber: value, - paymentReference: transactionData.transactionReference!, - appointmentNo: "0", - onSuccess: (val) { - emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true); - LoaderBottomSheet.hideLoader(); - if (emergencyServicesViewModel.isERBookAppointment) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)), - callBackFunc: () { - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } else {} - }); - }); - } else {} - }, onError: (errMsg) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: errMsg.toString()), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); + await emergencyServicesViewModel.ER_CreateAdvancePayment( + paymentMethodName: selectedPaymentMethod, + paymentReference: transactionData.transactionReference!, + onSuccess: (value) async { + await emergencyServicesViewModel.addAdvanceNumberRequest( + advanceNumber: value, + paymentReference: transactionData.transactionReference!, + appointmentNo: "0", + onSuccess: (val) { + emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true); + LoaderBottomSheet.hideLoader(); + if (emergencyServicesViewModel.isERBookAppointment) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)), + callBackFunc: () { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else {} + }); + }); + } else {} + }, + onError: (errMsg) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: errMsg.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); } else { openPaymentURL("visa"); } @@ -332,7 +330,7 @@ class _ErOnlineCheckinPaymentPageState extends State ).paddingSymmetrical(16.h, 16.h), ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "TAMARA"; - openPaymentURL("tamara"); + openPaymentURL("TAMARA"); }) : SizedBox.shrink(), ], @@ -353,39 +351,39 @@ class _ErOnlineCheckinPaymentPageState extends State children: [ (emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.isCash ?? true) ? Container( - height: 50.h, - decoration: ShapeDecoration( - color: AppColors.secondaryLightRedBorderColor, - shape: SmoothRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), - smoothness: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.secondaryLightRedBorderColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true).paddingSymmetrical(24.h, 0.h), CustomButton( - text: LocaleKeys.updateInsurance.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: InsuranceHomePage(), - ), - ); - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.secondaryLightRedBorderColor, - textColor: AppColors.whiteColor, - fontSize: 10, - isBold: true, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(15, 0, 15, 0), - height: 30.h, - ).paddingSymmetrical(24.h, 0.h), - ], - ), - ) + text: LocaleKeys.updateInsurance.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.secondaryLightRedBorderColor, + textColor: AppColors.whiteColor, + fontSize: 10, + isBold: true, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), + ], + ), + ) : const SizedBox(), SizedBox(height: 24.h), LocaleKeys.totalAmountToPay.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.h, 0.h), @@ -403,7 +401,9 @@ class _ErOnlineCheckinPaymentPageState extends State children: [ LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor), Utils.getPaymentAmountWithSymbol( - emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientTaxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientTaxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor), + AppColors.greyTextColor, + 13, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), @@ -417,14 +417,8 @@ class _ErOnlineCheckinPaymentPageState extends State ], ).paddingSymmetrical(24.h, 0.h), Platform.isIOS - ? Utils.buildSvgWithAssets( - icon: AppAssets.apple_pay_button, - width: 200.h, - height: 80.h, - fit: BoxFit.contain, - applyThemeColor: false - ).paddingSymmetrical(24.h, 0.h).onPress(() { - if (Utils.havePrivilege(103)) { + ? Utils.buildSvgWithAssets(icon: AppAssets.apple_pay_button, width: 200.h, height: 80.h, fit: BoxFit.contain, applyThemeColor: false).paddingSymmetrical(24.h, 0.h).onPress(() { + if (Utils.havePrivilege(103)) { startApplePay(); } else { openPaymentURL("ApplePay"); @@ -530,9 +524,7 @@ class _ErOnlineCheckinPaymentPageState extends State if (appState.isPaytabsEnabled) { selectedPaymentMethod = "VISA"; LoaderBottomSheet.hideLoader(); - paytabsViewModel.setPaymentConfiguration( - "ER Online Check-In Payment", - emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, + paytabsViewModel.setPaymentConfiguration("ER Online Check-In Payment", emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!, ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum(), emergencyServicesViewModel.selectedHospital!.iD); paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 1f8def35..367a9b51 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -159,9 +159,11 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { Clarity.setCustomUserId(appState.getAuthenticatedUser()!.patientId.toString()); - habibWalletVM.initHabibWalletProvider(); - habibWalletVM.getPatientBalanceAmount(); - habibWalletVM.getLakumAccountInformation(); + // habibWalletVM.initHabibWalletProvider(); + // Wallet & Lakum data are fetched lazily when the user unhides the values. + // habibWalletVM.getPatientBalanceAmount(); + // habibWalletVM.getLakumAccountInformation(); + todoSectionViewModel.initializeTodoSectionViewModel(); immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); @@ -234,9 +236,9 @@ class _LandingPageState extends State { myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); // Refresh Appointments Data - habibWalletVM.initHabibWalletProvider(); - habibWalletVM.getPatientBalanceAmount(); - habibWalletVM.getLakumAccountInformation(); + // habibWalletVM.initHabibWalletProvider(); + // habibWalletVM.getPatientBalanceAmount(); + // habibWalletVM.getLakumAccountInformation(); // Refresh Ancillary Orders todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); @@ -952,7 +954,7 @@ class _LandingPageState extends State { children: [ LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), SizedBox( - height: 175.h, + height: 180.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 2, diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 0bfba696..803b9b5a 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -59,26 +59,19 @@ class HabibWalletCard extends StatelessWidget { // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ - LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), - // Container( - // height: 40.h, - // width: 40.h, - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.textColor, - // borderRadius: 8.h, - // ), - // child: Padding( - // padding: EdgeInsets.all(8.h), - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.show_icon, - // width: 12.h, - // height: 12.h, - // fit: BoxFit.contain, - // ), - // ), - // ), - // ], - // ), + Consumer(builder: (context, habibWalletVM, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), + Icon( + habibWalletVM.isWalletAmountHidden ? Icons.visibility_off : Icons.visibility, + color: AppColors.textColor, + size: 24.h, + ).onPress(() => habibWalletVM.toggleWalletAmountVisibility()), + ], + ); + }), SizedBox(height: 4.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -98,9 +91,9 @@ class HabibWalletCard extends StatelessWidget { fit: BoxFit.contain, ), SizedBox(width: 8.h), - NumberFormat.decimalPattern() - .format(habibWalletVM.habibWalletAmount) - .toString() + (habibWalletVM.isWalletAmountHidden + ? "••••" + : NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount).toString()) .toText32(isBold: true, isEnglishOnly: true) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 40.h), ], diff --git a/lib/presentation/home/widgets/lakum_wallet_card.dart b/lib/presentation/home/widgets/lakum_wallet_card.dart index 3645a2d3..feea02ba 100644 --- a/lib/presentation/home/widgets/lakum_wallet_card.dart +++ b/lib/presentation/home/widgets/lakum_wallet_card.dart @@ -67,26 +67,19 @@ class LakumWalletCard extends StatelessWidget { // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), - LocaleKeys.lakumPoints.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), - // Container( - // height: 40.h, - // width: 40.h, - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.textColor, - // borderRadius: 8.h, - // ), - // child: Padding( - // padding: EdgeInsets.all(8.h), - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.show_icon, - // width: 12.h, - // height: 12.h, - // fit: BoxFit.contain, - // ), - // ), - // ), - // ], - // ), + Consumer(builder: (context, habibWalletVM, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.lakumPoints.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), + Icon( + habibWalletVM.isLakumPointsHidden ? Icons.visibility_off : Icons.visibility, + color: AppColors.textColor, + size: 24.h, + ).onPress(() => habibWalletVM.toggleLakumPointsVisibility()), + ], + ); + }), SizedBox(height: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -98,13 +91,15 @@ class LakumWalletCard extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - NumberFormat.decimalPattern() - .format(habibWalletVM.isLakumAccountInfoLoading - ? 123 - : habibWalletVM.yahalaAccountNumber == "0" - ? 0 - : (habibWalletVM.lakumAccountInfo?.pointsBalance ?? 0)) - .toString() + (habibWalletVM.isLakumPointsHidden + ? "••••" + : NumberFormat.decimalPattern() + .format(habibWalletVM.isLakumAccountInfoLoading + ? 123 + : habibWalletVM.yahalaAccountNumber == "0" + ? 0 + : (habibWalletVM.lakumAccountInfo?.pointsBalance ?? 0)) + .toString()) .toText32(isBold: true, isEnglishOnly: true) .toShimmer2(isShow: habibWalletVM.isLakumAccountInfoLoading, radius: 12.h, width: 80.h, height: 40.h), SizedBox(width: 4.h), diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index 7f4836f6..fe08fbbe 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -187,7 +187,7 @@ class MyInAppBrowser extends InAppBrowser { // if (context != null) GifLoaderDialogUtils.hideDialog(context); // AppToast.showErrorToast(message: err); // }); - } else if (paymentMethod == "TAMARA") { + } else if (paymentMethod.toLowerCase() == "tamara") { // LiveCareService service = new LiveCareService(); TamaraRequestModel tamaraRequestModel = new TamaraRequestModel(); From e96f2c3ecd13c7b99aa0c20ff593b3fb2a807e3b Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 14 Sep 2026 10:21:54 +0300 Subject: [PATCH 58/73] Habib Wallet & Lakum data updates --- .../habib_wallet/habib_wallet_view_model.dart | 15 +++++++++++++-- lib/widgets/app_language_change.dart | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index e888600a..21ec0e79 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -31,6 +31,7 @@ class HabibWalletViewModel extends ChangeNotifier { num walletRechargeAmount = 0; String notesText = ""; bool isWalletAmountToBeLoaded = false; + bool isLakumInfoToBeLoaded = false; // ── Balance visibility (show/hide like a password field) ────────────────── bool isWalletAmountHidden = true; @@ -55,7 +56,7 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); // On first unhide, fetch the data if not already fetched. if (!isLakumPointsHidden && !isLakumInfoFetched) { - isWalletAmountToBeLoaded = true; + isLakumInfoToBeLoaded = true; getLakumAccountInformation(); } } @@ -358,6 +359,8 @@ class HabibWalletViewModel extends ChangeNotifier { if (isWalletAmountToBeLoaded) { isWalletAmountLoading = true; habibWalletAmount = 0; + } + if (isLakumInfoToBeLoaded) { isLakumAccountInfoLoading = true; yahalaAccountNumber = ""; } @@ -429,6 +432,11 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + setIsLakumInfoToBeLoaded(bool value) { + isLakumInfoToBeLoaded = value; + notifyListeners(); + } + String getSelectedRechargeTypeValue() { switch (selectedRechargeType) { case 1: @@ -571,7 +579,7 @@ class HabibWalletViewModel extends ChangeNotifier { } Future getLakumAccountInformation({Function(dynamic)? onSuccess, Function(String)? onError}) async { - if (!isWalletAmountToBeLoaded) { + if (!isLakumInfoToBeLoaded) { return; } @@ -586,6 +594,8 @@ class HabibWalletViewModel extends ChangeNotifier { (failure) async { yahalaAccountNumber = "0"; isLakumAccountInfoLoading = false; + isLakumInfoFetched = true; + isLakumInfoToBeLoaded = false; notifyListeners(); // 675534 }, (apiResponse) async { @@ -613,6 +623,7 @@ class HabibWalletViewModel extends ChangeNotifier { } isLakumAccountInfoLoading = false; isLakumInfoFetched = true; + isLakumInfoToBeLoaded = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/widgets/app_language_change.dart b/lib/widgets/app_language_change.dart index c2ae0272..7810457b 100644 --- a/lib/widgets/app_language_change.dart +++ b/lib/widgets/app_language_change.dart @@ -76,6 +76,7 @@ class _AppLanguageChangeState extends State { myAppointmentsViewModel.getPatientAppointments(true, false); habibWalletVM.setIsWalletAmountToBeLoaded(true); + habibWalletVM.setIsLakumInfoToBeLoaded(true); habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); habibWalletVM.getLakumAccountInformation(); From 67bdc6123b1459fcaf930cdfc5f4c9087b8a4a8d Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 14 Sep 2026 10:52:35 +0300 Subject: [PATCH 59/73] Insurance model updates --- lib/presentation/home/landing_page.dart | 5 ++++- lib/presentation/medical_file/medical_file_page.dart | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 367a9b51..2fbd9417 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -172,7 +172,10 @@ class _LandingPageState extends State { emergencyServicesViewModel.checkPatientERAdvanceBalance(); // myAppointmentsViewModel.getPatientAppointmentQueueDetails(); notificationsViewModel.initNotificationsViewModel(); - insuranceViewModel.initInsuranceProvider(); + + if (Utils.havePrivilege(122)) { + insuranceViewModel.initInsuranceProvider(); + } if (!appState.isRatedVisible) { appointmentRatingViewModel.getLastRatingAppointment( diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index a3de6c08..27978723 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -110,7 +110,7 @@ class _MedicalFilePageState extends State { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - insuranceViewModel.initInsuranceProvider(); + // insuranceViewModel.initInsuranceProvider(); // Load vital signs hmgServicesViewModel.getPatientVitalSign(); @@ -495,6 +495,9 @@ class _MedicalFilePageState extends State { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); } + if (index == 2 && isExpanded) { + insuranceViewModel.initInsuranceProvider(); + } }, items: [ ExpandableListItem( From a02af3897f0eddb0f43d00a67030f554da880189 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 14 Sep 2026 12:02:38 +0300 Subject: [PATCH 60/73] Update to Stores VersionID 22.0 --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- lib/features/insurance/insurance_repo.dart | 8 +++++--- lib/features/insurance/insurance_view_model.dart | 8 ++++---- lib/presentation/home/landing_page.dart | 2 +- pubspec.yaml | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 4db0cf8b..784c2f44 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1307867; + // body['PatientID'] = 226077; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 094fa680..40f04b1e 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.9; + static final double appVersionID = 22.0; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/features/insurance/insurance_repo.dart b/lib/features/insurance/insurance_repo.dart index 2719035b..0a0ee0be 100644 --- a/lib/features/insurance/insurance_repo.dart +++ b/lib/features/insurance/insurance_repo.dart @@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/features/insurance/models/resp_models/upload import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class InsuranceRepo { - Future>>> getPatientInsuranceDetails(); + Future>>> getPatientInsuranceDetails({bool isFromHomePage = false}); Future>>> getPatientInsuranceCardHistory(); @@ -35,8 +35,10 @@ class InsuranceRepoImp implements InsuranceRepo { InsuranceRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientInsuranceDetails() async { - Map mapDevice = {}; + Future>>> getPatientInsuranceDetails({bool isFromHomePage = false}) async { + Map mapDevice = { + "IsFromHomePage": isFromHomePage + }; try { GenericApiModel>? apiResponse; diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 2422920a..caeba030 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -32,11 +32,11 @@ class InsuranceViewModel extends ChangeNotifier { InsuranceViewModel({required this.insuranceRepo, required this.errorHandlerService}); - initInsuranceProvider() { + initInsuranceProvider({bool isFromHomePage = false}) { if (isInsuranceDataToBeLoaded) { patientInsuranceList.clear(); isInsuranceLoading = true; - getPatientInsuranceDetails(); + getPatientInsuranceDetails(isFromHomePage: isFromHomePage); } patientInsuranceCardHistoryList.clear(); isInsuranceHistoryLoading = true; @@ -76,12 +76,12 @@ class InsuranceViewModel extends ChangeNotifier { notifyListeners(); } - Future getPatientInsuranceDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPatientInsuranceDetails({bool isFromHomePage = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { if (!isInsuranceDataToBeLoaded) { return; } - final result = await insuranceRepo.getPatientInsuranceDetails(); + final result = await insuranceRepo.getPatientInsuranceDetails(isFromHomePage: isFromHomePage); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 2fbd9417..fdb8afcf 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -174,7 +174,7 @@ class _LandingPageState extends State { notificationsViewModel.initNotificationsViewModel(); if (Utils.havePrivilege(122)) { - insuranceViewModel.initInsuranceProvider(); + insuranceViewModel.initInsuranceProvider(isFromHomePage: true); } if (!appState.isRatedVisible) { diff --git a/pubspec.yaml b/pubspec.yaml index e471cada..82ae76ac 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.45+46 -version: 0.0.15+2 +version: 0.0.48+49 +#version: 0.0.16+1 environment: sdk: ">=3.6.0 <4.0.0" From c03da1c541e7c77c9b6be4ba20bd5b9be03a438f Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 15 Sep 2026 12:28:11 +0300 Subject: [PATCH 61/73] Locale fix --- .../book_appointments_view_model.dart | 111 ++++++++++-------- lib/main.dart | 2 + .../book_appointment_page.dart | 2 + 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index dbed528e..a4530eee 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -842,67 +843,73 @@ class BookAppointmentsViewModel extends ChangeNotifier { } Future getDoctorFreeSlots({bool isBookingForLiveCare = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { - docFreeSlots.clear(); - DateTime date; - final DateFormat formatter = DateFormat('HH:mm', "en-US"); - final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); - Map _eventsParsed; - - final result = await bookAppointmentsRepo.getDoctorFreeSlots( - selectedDoctor.clinicID ?? 0, - selectedDoctor.projectID ?? 0, - selectedDoctor.doctorID ?? 0, - isBookingForLiveCare, - continueDentalPlan: isContinueDentalPlan, - onError: onError, - ); - - result.fold( - (failure) async { - print(failure); - if (onError != null) { - onError(failure.message); - } - }, - (apiResponse) { - if (apiResponse.messageStatus == 2) { - onError!(apiResponse.errorMessage ?? "Unknown error occurred"); - // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - if (apiResponse.data['FreeTimeSlots'] == null || apiResponse.data['FreeTimeSlots'].isEmpty) { - onError!("No free slots available".tr()); - return; + // try { + docFreeSlots.clear(); + DateTime date; + final DateFormat formatter = DateFormat('HH:mm', "en-US"); + // final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); + // Map _eventsParsed; + + final result = await bookAppointmentsRepo.getDoctorFreeSlots( + selectedDoctor.clinicID ?? 0, + selectedDoctor.projectID ?? 0, + selectedDoctor.doctorID ?? 0, + isBookingForLiveCare, + continueDentalPlan: isContinueDentalPlan, + onError: onError, + ); + + result.fold( + (failure) async { + print(failure); + if (onError != null) { + onError(failure.message); } - initialSlotDuration = apiResponse.data["InitialSlotDuration"]; - freeSlotsResponse = apiResponse.data['FreeTimeSlots']; - isWaitingAppointmentAvailable = apiResponse.data["IsAllowToBookWaitingAppointment"]; - - // isWaitingAppointmentAvailable = true; - - freeSlotsResponse.forEach((element) { - // date = (isLiveCareSchedule != null && isLiveCareSchedule) - // ? DateUtil.convertStringToDate(element) - // : - date = DateUtil.convertStringToDateSaudiTimezone(element, int.parse(selectedDoctor.projectID.toString())); - slotsList.add(FreeSlot(date, ['slot'])); - docFreeSlots.add(TimeSlot(isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); - }); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data['FreeTimeSlots'] == null || apiResponse.data['FreeTimeSlots'].isEmpty) { + onError!("No free slots available".tr()); + return; + } + initialSlotDuration = apiResponse.data["InitialSlotDuration"]; + freeSlotsResponse = apiResponse.data['FreeTimeSlots']; + isWaitingAppointmentAvailable = apiResponse.data["IsAllowToBookWaitingAppointment"]; + + // isWaitingAppointmentAvailable = true; + + freeSlotsResponse.forEach((element) { + // date = (isLiveCareSchedule != null && isLiveCareSchedule) + // ? DateUtil.convertStringToDate(element) + // : + date = DateUtil.convertStringToDateSaudiTimezone(element, int.parse(selectedDoctor.projectID.toString())); + slotsList.add(FreeSlot(date, ['slot'])); + docFreeSlots.add(TimeSlot(isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); + }); - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } } - } - }, - ); + }, + ); + // } catch(ex) { + // if (onError != null) { + // onError(ex.toString()); + // } + // } } Future getLiveCareDoctorFreeSlots({bool isBookingForLiveCare = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { docFreeSlots.clear(); DateTime date; final DateFormat formatter = DateFormat('HH:mm', "en-US"); - final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); - Map _eventsParsed; + // final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); + // Map _eventsParsed; final result = await bookAppointmentsRepo.getLiveCareDoctorFreeSlots( selectedDoctor.clinicID ?? 0, selectedLiveCareClinic.serviceID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, isBookingForLiveCare, diff --git a/lib/main.dart b/lib/main.dart index 223103ae..4e60c2a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -117,6 +117,8 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + Intl.defaultLocale = "en_US"; + SafeDevice.init( SafeDeviceConfig(mockLocationCheckEnabled: false), // disables mock location check on Android ); diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 53d4b1b1..c2560ecd 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -523,6 +523,7 @@ class _BookAppointmentPageState extends State { ], ).onPress(() { bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false); + bookAppointmentsViewModel.setIsLiveCareSchedule(false); bookAppointmentsViewModel.setProjectID(null); Navigator.of(context).push( CustomPageRoute( @@ -554,6 +555,7 @@ class _BookAppointmentPageState extends State { ).onPress(() { Clarity.sendCustomEvent('Select_Region_Page'); bookAppointmentsViewModel.setProjectID(null); + bookAppointmentsViewModel.setIsLiveCareSchedule(false); openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); }), ], From 34b671236efd84493a0a94bd3791583983228b7a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 15 Sep 2026 15:50:52 +0300 Subject: [PATCH 62/73] Updates --- lib/core/api/api_client.dart | 2 +- .../immediate_livecare/immediate_livecare_view_model.dart | 4 +++- lib/presentation/book_appointment/search_doctor_by_name.dart | 2 +- lib/presentation/book_appointment/widgets/doctor_card.dart | 2 +- lib/splashPage.dart | 4 ++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 784c2f44..4c961843 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 226077; + // body['PatientID'] = 5422188; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index e2e8d2cb..57bf6553 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -135,7 +135,9 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { result.fold( (failure) async { - onError!(failure.message); + if(onError != null) { + onError(failure.message); + } }, (apiResponse) { if (apiResponse.messageStatus == 2) { diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 4e9f1435..f1f391b0 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -218,7 +218,7 @@ class _SearchDoctorByNameState extends State { bookAppointmentsViewModel: bookAppointmentsViewModel, isDoctorNameSearch: true, ).paddingSymmetrical(16.h, 0.h).onPress(() async { - if (bookAppointmentsVM.filteredDoctorList[index].clinicID == 17 && getIt.get().getAuthenticatedUser()!.age! < 12) { + if (getIt.get().isAuthenticated && bookAppointmentsVM.filteredDoctorList[index].clinicID == 17 && getIt.get().getAuthenticatedUser()!.age! < 12) { bookAppointmentsViewModel.setProjectID(bookAppointmentsVM.filteredDoctorList[index].projectID.toString()); bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( clinicID: bookAppointmentsVM.filteredDoctorList[index].clinicID, diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index d2f431a9..23a9cbcd 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -186,7 +186,7 @@ class DoctorCard extends StatelessWidget { if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 17) { GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); - if (getIt.get().getAuthenticatedUser()!.age! > 12) { + if (getIt.get().isAuthenticated && getIt.get().getAuthenticatedUser()!.age! > 12) { bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); bookAppointmentsViewModel.setSelectedClinic(selectedClinic); bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index fe223fc2..cf407d41 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -116,8 +116,8 @@ class _SplashScreenState extends State { logLevel: LogLevel.Error, // Optional: Set the log level (Verbose, Debug, Info, Warning, Error, None) ); - Clarity.initialize(context, config); - + Clarity.initialize(getIt.get().navigatorKey.currentContext!, config); + // Set initial app context tags try { final appState = getIt.get(); From 648cdafacd4bb69a8748a19660e8025b61a9e233 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 16 Sep 2026 11:15:01 +0300 Subject: [PATCH 63/73] Update to stores Version 22.1 --- lib/core/api_consts.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 40f04b1e..45d6f48c 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 22.0; + static final double appVersionID = 22.1; // static final double appVersionID = 50.7; static final int appChannelId = 3; From 445d4d938502aff21b1dbce1eeb97703b47ff540 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 20 Sep 2026 10:01:57 +0300 Subject: [PATCH 64/73] Updates --- pubspec.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 82ae76ac..c030a1f4 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.48+49 -#version: 0.0.16+1 +version: 0.0.49+50 +#version: 0.0.17+1 environment: sdk: ">=3.6.0 <4.0.0" @@ -113,6 +113,7 @@ dependencies: cloudflare_turnstile: ^3.7.2 freerasp: ^8.2.1 safe_device: ^1.4.1 + myfatoorah_flutter: ^3.3.2 dev_dependencies: flutter_test: From a4e1ec4459cb947cdc72dcad6ff74c1b8a275f0e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 20 Sep 2026 10:02:38 +0300 Subject: [PATCH 65/73] updates --- lib/core/api_consts.dart | 17 ++++++++++++++++- lib/core/app_state.dart | 6 ++++++ .../my_appointments_view_model.dart | 6 +++--- .../book_appointment/widgets/doctor_card.dart | 3 +++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 45d6f48c..712866af 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -1,6 +1,7 @@ import 'package:amazon_payfort/amazon_payfort.dart'; import 'package:flutter_amazonpaymentservices/environment_type.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; class ApiConsts { static const maxSmallScreen = 660; @@ -44,12 +45,17 @@ class ApiConsts { // var payFortEnvironment = FortEnvironment.test; // var applePayMerchantId = "merchant.com.hmgwebservices.uat"; + static var myFatoorahEnvironmentType = MFEnvironment.TEST; + static var myFatoorahCountry = MFCountry.SAUDIARABIA; + static setBackendURLs() { switch (appEnvironmentType) { case AppEnvironmentTypeEnum.prod: baseUrl = "https://hmgwebservices.com/"; payFortEnvironment = FortEnvironment.production; payFortEnvironmentType = EnvironmentType.production; + myFatoorahEnvironmentType = MFEnvironment.LIVE; + myFatoorahCountry = MFCountry.SAUDIARABIA; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; @@ -66,6 +72,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -77,12 +85,13 @@ class ApiConsts { symptomsCheckerApi = symptomsCheckerApiUAT; symptomsCheckerUsername = 'guest_user'; symptomsCheckerPassword = '123456'; - break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -100,6 +109,8 @@ class ApiConsts { baseUrl = "https://webservices.hmg.com/"; payFortEnvironment = FortEnvironment.production; payFortEnvironmentType = EnvironmentType.production; + myFatoorahEnvironmentType = MFEnvironment.LIVE; + myFatoorahCountry = MFCountry.SAUDIARABIA; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -117,6 +128,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -134,6 +147,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 0d015922..ef5e306b 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -48,12 +48,18 @@ class AppState { bool isPaytabsEnabled = false; bool isPayfortHostedPageEnabled = true; + bool isMyFatoorahEnabled = false; void setIsPaytabsEnabled(bool value) { isPaytabsEnabled = value; // isPaytabsEnabled = true; } + void setIsMyFatoorahEnabled(bool value) { + isMyFatoorahEnabled = value; + // isMyFatoorahEnabled = true; + } + void setIsPayfortHostedPageEnabled(bool value) { isPayfortHostedPageEnabled = value; // isPayfortHostedPageEnabled = false; diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index b153c556..b2b990db 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -772,9 +772,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; - if (!isTop8) { - isMyDoctorsDataToBeLoaded = false; - } + // if (!isTop8) { + isMyDoctorsDataToBeLoaded = false; + // } notifyListeners(); if (onSuccess != null) { diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 23a9cbcd..b14a3822 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -219,6 +219,9 @@ class DoctorCard extends StatelessWidget { ); } else { bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel); + GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( + clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); LoaderBottomSheet.showLoader(); bookAppointmentsViewModel.isLiveCareSchedule ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( From b78c83a32dce471b81418e8fc15fb59f8b8ce889 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 20 Sep 2026 11:02:29 +0300 Subject: [PATCH 66/73] updates --- lib/core/api/api_client.dart | 2 +- lib/features/lab/lab_view_model.dart | 12 ++++++------ lib/features/myfatoorah/myfatoorah_view_model.dart | 13 +++++++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 lib/features/myfatoorah/myfatoorah_view_model.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 4c961843..f9cf2fa2 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 5422188; + // body['PatientID'] = 1987471; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 1c25ef9d..d7f5bb62 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -209,12 +209,12 @@ class LabViewModel extends ChangeNotifier { final result = await labRepo.getLabResultReportPDF(labOrder: labOrder); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), + // (failure) async => await errorHandlerService.handleError( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, (apiResponse) { if (apiResponse.messageStatus == 2) { onError!(apiResponse.errorMessage!); diff --git a/lib/features/myfatoorah/myfatoorah_view_model.dart b/lib/features/myfatoorah/myfatoorah_view_model.dart new file mode 100644 index 00000000..8a4160e9 --- /dev/null +++ b/lib/features/myfatoorah/myfatoorah_view_model.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class MyFatoorahViewModel extends ChangeNotifier { + final AppState appState; + final ErrorHandlerService errorHandlerService; + + MyFatoorahViewModel({ + required this.appState, + required this.errorHandlerService, + }); +} From d43022bbb4ece625d945723bc1f000c19d1f4f42 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 20 Sep 2026 17:07:21 +0300 Subject: [PATCH 67/73] MyFatoorah Payment integration in progress --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- lib/core/app_state.dart | 5 ++- lib/core/dependencies.dart | 6 +++ lib/core/myFatoorah_config.dart | 5 +++ .../authentication_view_model.dart | 2 + .../myfatoorah/myfatoorah_view_model.dart | 42 +++++++++++++++++++ lib/main.dart | 4 ++ .../immediate_livecare_payment_page.dart | 25 ++++++++++- 9 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 lib/core/myFatoorah_config.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f9cf2fa2..d6448e8f 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; + body['TokenID'] = "@dm!n"; // body['PatientID'] = 1987471; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 712866af..588be9ec 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -6,7 +6,7 @@ import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index ef5e306b..78432b23 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -49,6 +49,7 @@ class AppState { bool isPaytabsEnabled = false; bool isPayfortHostedPageEnabled = true; bool isMyFatoorahEnabled = false; + // bool isMyFatoorahEnabled = true; void setIsPaytabsEnabled(bool value) { isPaytabsEnabled = value; @@ -56,8 +57,8 @@ class AppState { } void setIsMyFatoorahEnabled(bool value) { - isMyFatoorahEnabled = value; - // isMyFatoorahEnabled = true; + // isMyFatoorahEnabled = value; + isMyFatoorahEnabled = true; } void setIsPayfortHostedPageEnabled(bool value) { diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index b6c7fb38..a5093815 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -44,6 +44,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_rep import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_repo.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart'; @@ -380,6 +381,11 @@ class AppDependencies { errorHandlerService: getIt(), )); + getIt.registerLazySingleton(() => MyFatoorahViewModel( + appState: getIt(), + errorHandlerService: getIt(), + )); + getIt.registerLazySingleton( () => ClarityService(appState: getIt.get()), ); diff --git a/lib/core/myFatoorah_config.dart b/lib/core/myFatoorah_config.dart new file mode 100644 index 00000000..582ced3c --- /dev/null +++ b/lib/core/myFatoorah_config.dart @@ -0,0 +1,5 @@ +class MyFatoorahConfig { + + static const String configAPIKey = "SK_KWT_0auB76h3Frhj7aUBSZa6H5ei792eZoGAPh0bBxKsrgtyjqKgf38MPnZqzGqagErb"; + +} \ No newline at end of file diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index a8719f29..271c628d 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1248,6 +1248,7 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; _appState.setIsPaytabsEnabled(Utils.havePrivilege(120)); _appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121)); + _appState.setIsMyFatoorahEnabled(Utils.havePrivilege(123)); // Refresh privileges after user switching to ensure correct privileges are loaded if (isSwitchUser) { @@ -1796,6 +1797,7 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setProjectsDetailList(projectDetailListModel); _appState.setIsPaytabsEnabled(Utils.havePrivilege(120)); _appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121)); + _appState.setIsMyFatoorahEnabled(Utils.havePrivilege(123)); } }, ); diff --git a/lib/features/myfatoorah/myfatoorah_view_model.dart b/lib/features/myfatoorah/myfatoorah_view_model.dart index 8a4160e9..4e91f094 100644 --- a/lib/features/myfatoorah/myfatoorah_view_model.dart +++ b/lib/features/myfatoorah/myfatoorah_view_model.dart @@ -1,6 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/myFatoorah_config.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; class MyFatoorahViewModel extends ChangeNotifier { final AppState appState; @@ -10,4 +16,40 @@ class MyFatoorahViewModel extends ChangeNotifier { required this.appState, required this.errorHandlerService, }); + + // Payment Method ID + // 2: Credit/Debit Card + // 6: Mada + + initiateCardPayment({ + num amount = 10.0, + paymentDescription = "", + String displayCurrencyIso = MFCurrencyISO.KUWAIT_KWD, + Function(MFGetPaymentStatusResponse)? onSuccess, + Function(String)? onError, + }) async { + LoaderBottomSheet.showLoader(); + + await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); + + MFSDK.setUpActionBar(toolBarTitle: paymentDescription, toolBarTitleColor: "#ffffff", toolBarBackgroundColor: '#ED1C2B', isShowToolBar: true); + + var request = MFExecutePaymentRequest(paymentMethodId: 2, invoiceValue: amount); + request.displayCurrencyIso = displayCurrencyIso; + request.customerName = ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? "Test test" : "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + request.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId}@HMG.com"; + + await MFSDK.executePayment(request, appState.isArabic() ? MFLanguage.ARABIC : MFLanguage.ENGLISH, (invoiceId) { + LoaderBottomSheet.hideLoader(); + print(invoiceId); + }).then((value) { + if (onSuccess != null) { + onSuccess(value); + } + }).catchError((error) { + if (onError != null) { + onError(error.message); + } + }); + } } diff --git a/lib/main.dart b/lib/main.dart index 4e60c2a0..0fb6f773 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,6 +35,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_ import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; @@ -303,6 +304,9 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), ) 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 31defa56..dfb7497c 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -33,6 +34,7 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:myfatoorah_flutter/MFEnums.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -49,6 +51,7 @@ class _ImmediateLiveCarePaymentPageState extends State(context, listen: false); payfortViewModel = Provider.of(context, listen: false); paytabsViewModel = Provider.of(context, listen: false); + myFatoorahViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, myAppointmentsVM, child) { @@ -298,7 +302,26 @@ class _ImmediateLiveCarePaymentPageState extends State Date: Mon, 21 Sep 2026 17:35:38 +0300 Subject: [PATCH 68/73] MyFatoorah implementation contd. --- lib/core/api/api_client.dart | 2 +- lib/core/dependencies.dart | 1 + .../myfatoorah/myfatoorah_view_model.dart | 150 +++++++++++++++++- .../immediate_livecare_payment_page.dart | 133 ++++++++++++++-- 4 files changed, 268 insertions(+), 18 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index d6448e8f..da0954d4 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1987471; + // body['PatientID'] = 7000792; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a5093815..37fd936b 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -384,6 +384,7 @@ class AppDependencies { getIt.registerLazySingleton(() => MyFatoorahViewModel( appState: getIt(), errorHandlerService: getIt(), + payfortRepo: getIt(), )); getIt.registerLazySingleton( diff --git a/lib/features/myfatoorah/myfatoorah_view_model.dart b/lib/features/myfatoorah/myfatoorah_view_model.dart index 4e91f094..fb7a23fa 100644 --- a/lib/features/myfatoorah/myfatoorah_view_model.dart +++ b/lib/features/myfatoorah/myfatoorah_view_model.dart @@ -1,8 +1,18 @@ +import 'dart:developer'; + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/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/myFatoorah_config.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.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/generated/codegen_loader.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; @@ -11,10 +21,12 @@ import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; class MyFatoorahViewModel extends ChangeNotifier { final AppState appState; final ErrorHandlerService errorHandlerService; + final PayfortRepo payfortRepo; MyFatoorahViewModel({ required this.appState, required this.errorHandlerService, + required this.payfortRepo, }); // Payment Method ID @@ -25,26 +37,94 @@ class MyFatoorahViewModel extends ChangeNotifier { num amount = 10.0, paymentDescription = "", String displayCurrencyIso = MFCurrencyISO.KUWAIT_KWD, + required String paymentOption, + required String liveServiceID, + required String isScheduleLiveCare, + required String appointmentDate, + required String appointmentNo, + required String doctorID, + required String fileNumber, + required String serviceID, + required String projectID, Function(MFGetPaymentStatusResponse)? onSuccess, Function(String)? onError, }) async { LoaderBottomSheet.showLoader(); - await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); MFSDK.setUpActionBar(toolBarTitle: paymentDescription, toolBarTitleColor: "#ffffff", toolBarBackgroundColor: '#ED1C2B', isShowToolBar: true); - var request = MFExecutePaymentRequest(paymentMethodId: 2, invoiceValue: amount); + var request = MFExecutePaymentRequest(paymentMethodId: paymentOption.toLowerCase() == "mada" ? 6 : 2, invoiceValue: amount); request.displayCurrencyIso = displayCurrencyIso; request.customerName = ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? "Test test" : "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; request.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId}@HMG.com"; - await MFSDK.executePayment(request, appState.isArabic() ? MFLanguage.ARABIC : MFLanguage.ENGLISH, (invoiceId) { + await MFSDK.executePayment(request, appState.isArabic() ? MFLanguage.ARABIC : MFLanguage.ENGLISH, (invoiceId) async { + PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel(); + + payfortRequestInsertModel.commandType = "SALE"; + payfortRequestInsertModel.amount = amount; + payfortRequestInsertModel.customerEmail = getIt.get().getAuthenticatedUser()!.emailAddress; + payfortRequestInsertModel.orderDescription = paymentDescription; + payfortRequestInsertModel.serviceID = serviceID; + payfortRequestInsertModel.paymentOption = paymentOption; + payfortRequestInsertModel.clientRequestID = invoiceId; + 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 = invoiceId; + payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + payfortRequestInsertModel.language = getIt.get().isArabic() ? "ar" : "en"; + payfortRequestInsertModel.integrationId = "6"; // 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(); - print(invoiceId); - }).then((value) { - if (onSuccess != null) { - onSuccess(value); + }).then((value) async { + var valJson = value.toString(); + print(valJson); + + // // Create PayfortResponseInsertModel (same as Payfort implementation) + PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel(); + + payfortResponseInsertModel.fortId = value.invoiceTransactions!.first.transactionId ?? ""; + payfortResponseInsertModel.commandType = "SALE"; + // payfortResponseInsertModel.amount = paytabsTransactionResponseModel.tranTotal ?? 0; + payfortResponseInsertModel.amount = amount; + payfortResponseInsertModel.sadadOLP = ""; + payfortResponseInsertModel.paymentOption = value.invoiceTransactions!.first.paymentGateway ?? ""; + payfortResponseInsertModel.customerIP = ""; + payfortResponseInsertModel.eCI = ""; + payfortResponseInsertModel.responseMessage = value.invoiceTransactions!.first.transactionStatus ?? ""; + payfortResponseInsertModel.cardNumber = value.invoiceTransactions!.first.cardNumber ?? ""; + payfortResponseInsertModel.cardBin = ""; + payfortResponseInsertModel.installmentsNumber = 0; + payfortResponseInsertModel.status = (value.invoiceStatus ?? "").toLowerCase() == "paid" ? "14" : "00"; + payfortResponseInsertModel.merchantRef = value.invoiceId.toString(); + payfortResponseInsertModel.isRefund = false; + payfortResponseInsertModel.remmeberMe = true; + payfortResponseInsertModel.reconciliationReference = ""; + + // Call response insert API after payment + await paytabsResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); + + if ((value.invoiceStatus ?? "").toLowerCase() == "paid") { + if (onSuccess != null) { + onSuccess(value); + } + } else { + if (onError != null) { + onError(LocaleKeys.paymentFailedPleaseTryAgain.tr()); + } } }).catchError((error) { if (onError != null) { @@ -52,4 +132,60 @@ class MyFatoorahViewModel extends ChangeNotifier { } }); } + + initApplePayPayment(num invoiceValue, MFApplePayButton mfApplePayButton) async { + await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); + + await MFSDK.initSession(MFInitiateSessionRequest(), MFLanguage.ENGLISH).then((value) async { + var executePaymentRequest = MFExecutePaymentRequest(invoiceValue: 121, displayCurrencyIso: MFCurrencyISO.KUWAIT_KWD); + + await mfApplePayButton.executeApplePayButton( + executePaymentRequest, + (String invoiceId) { + print("Payment Success! Invoice ID: $invoiceId"); + }, + ).catchError((error) { + // If initialized properly, this error won't occur + print("Apple Pay Error: ${error.message}"); + }); + }).catchError((error) { + log(error.message); + }); + } + + 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); + } + } + }, + ); + } } 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 dfb7497c..40a79370 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -11,6 +11,7 @@ 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/myFatoorah_config.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; @@ -35,6 +36,7 @@ import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:myfatoorah_flutter/MFEnums.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -53,6 +55,8 @@ class _ImmediateLiveCarePaymentPageState extends State(); myAppointmentsViewModel = Provider.of(context, listen: false); immediateLiveCareViewModel = Provider.of(context, listen: false); @@ -135,7 +142,62 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (errMessage) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: errMessage.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else if (appState.isPaytabsEnabled) { transID = Utils.getAppointmentTransID( immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!, ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, @@ -311,7 +373,43 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, onError: (errMessage) { showCommonBottomSheetWithoutHeight( context, @@ -633,16 +731,31 @@ class _ImmediateLiveCarePaymentPageState extends State Date: Tue, 22 Sep 2026 11:49:37 +0300 Subject: [PATCH 69/73] theme code updated. --- assets/json/default_occasion_config.json | 11 ++ ios/Runner.xcodeproj/project.pbxproj | 8 -- lib/core/api_consts.dart | 2 +- lib/core/dependencies.dart | 10 ++ .../occasion_theme/occasion_config_model.dart | 117 ++++++++++++++++++ .../occasion_theme_view_model.dart | 102 +++++++++++++++ lib/main.dart | 21 +++- .../appointments/appointment_queue_page.dart | 2 +- .../widgets/ask_doctor_appointment_card.dart | 2 +- .../blood_donation/blood_donation_page.dart | 2 +- .../contact_us/feedback_page.dart | 2 +- lib/presentation/contact_us/find_us_page.dart | 2 +- .../hmg_services/services_page.dart | 2 +- lib/presentation/home/landing_page.dart | 14 ++- .../home/widgets/occasion_banner_widget.dart | 61 +++++++++ .../insurance_approval_details_page.dart | 2 +- .../widgets/insurance_approval_card.dart | 4 +- .../insurance/widgets/insurance_history.dart | 4 +- .../widgets/patient_insurance_card.dart | 4 +- .../eye_measurement_details_page.dart | 2 +- .../medical_file/medical_file_page.dart | 2 +- .../my_family/widget/family_cards.dart | 6 +- .../widgets/invoice_list_card.dart | 6 +- .../widgets/pharmacy_invoice_card.dart | 6 +- .../prescriptions/prescription_item_view.dart | 4 +- .../profile_settings/profile_settings.dart | 2 +- .../widgets/family_card_widget.dart | 2 +- .../refund_request/refund_history_screen.dart | 4 +- .../symptoms_checker/risk_factors_screen.dart | 2 +- .../symptoms_checker/suggestions_screen.dart | 2 +- .../symptoms_selector_screen.dart | 2 +- .../symptoms_checker/triage_screen.dart | 4 +- .../user_info_flow_manager.dart | 8 +- .../user_info_selection_screen.dart | 2 +- .../widgets/pinch_zoom_tutorial_overlay.dart | 2 +- .../widgets/triage_level_card.dart | 2 +- .../widgets/cup_bottomsheet_widgets.dart | 6 +- .../weather/weather_details_page.dart | 4 +- lib/services/remote_config_service.dart | 84 +++++++++++++ lib/theme/colors.dart | 24 +++- .../bottom_navigation/bottom_navigation.dart | 11 +- .../date_range_calender.dart | 10 +- lib/widgets/map/HMSMap.dart | 2 +- lib/widgets/map/gms_map.dart | 2 +- pubspec.yaml | 1 + 45 files changed, 502 insertions(+), 72 deletions(-) create mode 100644 assets/json/default_occasion_config.json create mode 100644 lib/features/occasion_theme/occasion_config_model.dart create mode 100644 lib/features/occasion_theme/occasion_theme_view_model.dart create mode 100644 lib/presentation/home/widgets/occasion_banner_widget.dart create mode 100644 lib/services/remote_config_service.dart diff --git a/assets/json/default_occasion_config.json b/assets/json/default_occasion_config.json new file mode 100644 index 00000000..37740e39 --- /dev/null +++ b/assets/json/default_occasion_config.json @@ -0,0 +1,11 @@ +{ + "key": "none", + "enabled": false, + "start_date": null, + "end_date": null, + "primary_color": null, + "background_opacity_image": null, + "background_image_opacity": 0.2, + "header_image": null +} + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f52d12fc..21dccceb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -339,14 +339,10 @@ 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"; @@ -360,14 +356,10 @@ 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"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 45d6f48c..68f2b21a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index b6c7fb38..b70de321 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -84,7 +84,9 @@ 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/services/remote_config_service.dart'; import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.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'; @@ -383,5 +385,13 @@ class AppDependencies { getIt.registerLazySingleton( () => ClarityService(appState: getIt.get()), ); + + // Remote-config-driven "occasion" theming (National Day, Eid, etc.). + getIt.registerLazySingleton(() => RemoteConfigServiceImp(loggerService: getIt())); + getIt.registerLazySingleton(() => OccasionThemeViewModel( + remoteConfigService: getIt(), + cacheService: getIt(), + loggerService: getIt(), + )); } } diff --git a/lib/features/occasion_theme/occasion_config_model.dart b/lib/features/occasion_theme/occasion_config_model.dart new file mode 100644 index 00000000..8a46d62b --- /dev/null +++ b/lib/features/occasion_theme/occasion_config_model.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +/// Model representing a remotely-configurable "occasion" theme +/// (e.g. Saudi National Day, Founding Day, Eid, Ramadan, ...). +/// +/// Everything here is fully data-driven — nothing about a specific +/// occasion (name, color, dates, banner asset) is hardcoded in the app. +/// All values are supplied via Firebase Remote Config (or the bundled +/// default JSON fallback used before the first successful fetch). +@immutable +class OccasionConfigModel { + /// Unique identifier of the occasion (e.g. "national_day"). Used only + /// for logging/analytics — no app logic branches on this value. + final String key; + + /// Master kill-switch. When false, the app must look exactly like the + /// default/current theme, regardless of any other field below. + final bool enabled; + + /// Optional activation window. When both are present, the theme is only + /// considered active if `DateTime.now()` falls within [startDate, endDate]. + /// When null, the [enabled] flag alone controls activation. + final DateTime? startDate; + final DateTime? endDate; + + /// Hex string (e.g. "#00894A") for the occasion's primary brand color. + /// When null, the app's default primary color is preserved. + final String? primaryColorHex; + + /// Remote (or bundled asset path) image rendered as a decorative + /// background/watermark, drawn using [backgroundImageOpacity]. + final String? backgroundOpacityImage; + + /// Opacity (0.0 - 1.0) applied to [backgroundOpacityImage]. + final double backgroundImageOpacity; + + /// Remote (or bundled asset path) image rendered as the occasion's main + /// header/banner (e.g. the National Day greeting banner), drawn at full + /// opacity on top of [backgroundOpacityImage]. + final String? headerImage; + + const OccasionConfigModel({ + required this.key, + required this.enabled, + this.startDate, + this.endDate, + this.primaryColorHex, + this.backgroundOpacityImage, + this.backgroundImageOpacity = 1.0, + this.headerImage, + }); + + /// Fully inert config — identical to the app's current/default look. + factory OccasionConfigModel.disabled() => const OccasionConfigModel( + key: 'none', + enabled: false, + startDate: null, + endDate: null, + primaryColorHex: null, + backgroundOpacityImage: null, + backgroundImageOpacity: 1.0, + headerImage: null, + ); + + factory OccasionConfigModel.fromJson(Map json) { + return OccasionConfigModel( + key: (json['key'] as String?) ?? 'none', + enabled: (json['enabled'] as bool?) ?? false, + startDate: _parseDate(json['start_date']), + endDate: _parseDate(json['end_date']), + primaryColorHex: json['primary_color'] as String?, + backgroundOpacityImage: json['background_opacity_image'] as String?, + backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0, + headerImage: json['header_image'] as String?, + ); + } + + static DateTime? _parseDate(dynamic value) { + if (value == null) return null; + if (value is String && value.isNotEmpty) { + return DateTime.tryParse(value); + } + return null; + } + + /// Whether "now" falls inside the configured activation window. + /// If no window is configured, the [enabled] flag alone decides. + bool isWithinActiveWindow([DateTime? now]) { + final current = now ?? DateTime.now(); + if (startDate != null && current.isBefore(startDate!)) return false; + if (endDate != null && current.isAfter(endDate!)) return false; + return true; + } + + /// Parsed [Color] from [primaryColorHex], or null if not configured. + Color? get primaryColor => _hexToColor(primaryColorHex); + + static Color? _hexToColor(String? hex) { + if (hex == null || hex.isEmpty) return null; + var value = hex.trim().replaceFirst('#', ''); + if (value.length == 6) value = 'FF$value'; + final parsed = int.tryParse(value, radix: 16); + return parsed != null ? Color(parsed) : null; + } + + Map toJson() => { + 'key': key, + 'enabled': enabled, + 'start_date': startDate?.toIso8601String(), + 'end_date': endDate?.toIso8601String(), + 'primary_color': primaryColorHex, + 'background_opacity_image': backgroundOpacityImage, + 'background_image_opacity': backgroundImageOpacity, + 'header_image': headerImage, + }; +} + diff --git a/lib/features/occasion_theme/occasion_theme_view_model.dart b/lib/features/occasion_theme/occasion_theme_view_model.dart new file mode 100644 index 00000000..b1016589 --- /dev/null +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_config_model.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'package:hmg_patient_app_new/services/remote_config_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Drives the fully remote-config-based "occasion" theming (National Day, +/// Founding Day, Eid, Ramadan, ...). Nothing about a specific occasion is +/// hardcoded here — every color/asset/date comes from [OccasionConfigModel], +/// which is parsed from Firebase Remote Config. +/// +/// Mirrors the existing dark-mode pattern in ProfileSettingsViewModel: +/// a static flag on [AppColors] is kept in sync so every widget using +/// AppColors.primaryRedColor (even ones with no BuildContext/Provider +/// access) reflects the active occasion instantly. +class OccasionThemeViewModel extends ChangeNotifier { + static const String _cachedConfigKey = 'cached_occasion_config_json'; + + final RemoteConfigService _remoteConfigService; + final CacheService _cacheService; + final LoggerService _loggerService; + + OccasionThemeViewModel({ + required RemoteConfigService remoteConfigService, + required CacheService cacheService, + required LoggerService loggerService, + }) : _remoteConfigService = remoteConfigService, + _cacheService = cacheService, + _loggerService = loggerService; + + OccasionConfigModel _config = OccasionConfigModel.disabled(); + OccasionConfigModel get config => _config; + + /// True only when enabled AND (if a window is configured) within it. + bool get isOccasionActive => _config.enabled && _config.isWithinActiveWindow(); + + String? get backgroundOpacityImage => isOccasionActive ? _config.backgroundOpacityImage : null; + + double get backgroundImageOpacity => _config.backgroundImageOpacity; + + /// The occasion's main header/banner image (e.g. National Day greeting + /// banner), rendered at full opacity. Null unless an occasion is active. + String? get headerImage => isOccasionActive ? _config.headerImage : null; + + /// Call once at app startup (before the first frame) to restore the last + /// known-good config instantly (offline-safe), then kick off a remote + /// fetch in the background. + Future initialize() async { + _restoreCachedConfig(); + _applyToAppColors(); + + try { + await _remoteConfigService.initialize(); + await refresh(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.initialize error: $ex'); + } + } + + /// Fetches the latest occasion config from Firebase Remote Config and + /// applies it. Safe to call again later (e.g. on app resume) to pick up + /// changes without requiring an app update. + Future refresh() async { + try { + await _remoteConfigService.fetchAndActivate(); + final rawJson = _remoteConfigService.occasionConfigRawJson; + _config = OccasionConfigModel.fromJson(json.decode(rawJson) as Map); + await _cacheService.saveString(key: _cachedConfigKey, value: rawJson); + _applyToAppColors(); + notifyListeners(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.refresh error: $ex'); + // Keep whatever config was already applied (cached/disabled) — never + // let a remote-config failure change the app's appearance unexpectedly. + } + } + + void _restoreCachedConfig() { + try { + final cachedJson = _cacheService.getString(key: _cachedConfigKey); + if (cachedJson == null || cachedJson.isEmpty) { + _config = OccasionConfigModel.disabled(); + return; + } + _config = OccasionConfigModel.fromJson(json.decode(cachedJson) as Map); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel._restoreCachedConfig error: $ex'); + _config = OccasionConfigModel.disabled(); + } + } + + /// Keeps AppColors in sync so non-Provider call sites (AppColors.primaryRedColor) + /// pick up the occasion color exactly like the existing dark-mode flag does. + void _applyToAppColors() { + AppColors.isOccasionThemeActive = isOccasionActive; + AppColors.occasionPrimaryColor = _config.primaryColor; + } +} + diff --git a/lib/main.dart b/lib/main.dart index 4e60c2a0..e5b538e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -36,6 +37,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_reg import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; @@ -166,6 +168,14 @@ Future callInitializations() async { // Restore persisted accessibility settings before the first frame. getIt.get().loadAccessibilitySettings(); + + // Restore last-known occasion theme instantly (offline-safe, synchronous) + // so the very first frame is already correct, then let the Remote Config + // fetch happen in the background without blocking app startup. Fully + // disabled by default — the app looks exactly as it does today until + // Remote Config enables an occasion (e.g. National Day) on the server side. + final occasionThemeViewModel = getIt.get(); + unawaited(occasionThemeViewModel.initialize()); } void main() async { @@ -305,7 +315,10 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ], child: MyApp()), ), // Wrap your app ); @@ -373,11 +386,11 @@ class MyApp extends StatelessWidget { return Sizer( builder: (context, orientation, deviceType) { // SizeConfig().init(constraints, orientation); - return Consumer( - builder: (context, profileVm, _) { + return Consumer2( + builder: (context, profileVm, occasionThemeVm, _) { final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; return MaterialApp( - key: ValueKey('app_theme_${profileVm.isDarkMode}'), + key: ValueKey('app_theme_${profileVm.isDarkMode}_${occasionThemeVm.isOccasionActive}_${occasionThemeVm.config.key}'), // title: 'Dr. AlHabib', title: 'Dr. AlHabib Beta', builder: (context, mchild) { diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart index f2bd1d18..031295e5 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -150,7 +150,7 @@ class AppointmentQueuePage extends StatelessWidget { textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), ), 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 e7d7c187..bd9fdef9 100644 --- a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart +++ b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart @@ -68,7 +68,7 @@ class AskDoctorAppointmentCard extends StatelessWidget { ), AppCustomChipWidget( labelText: LocaleKeys.outPatient.tr(context: context), - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), textColor: AppColors.primaryRedColor, ), AppCustomChipWidget( diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart index f3f0782d..62c409c6 100644 --- a/lib/presentation/blood_donation/blood_donation_page.dart +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -244,7 +244,7 @@ class _BloodDonationPageState extends State { Container( padding: EdgeInsets.all(8.h), decoration: BoxDecoration( - color: AppColors.primaryRedColor.withValues(alpha: 0.1), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8.r), ), child: Utils.buildSvgWithAssets( diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 91fa3b52..a5d68614 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -59,7 +59,7 @@ class FeedbackPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.send.tr(context: context)), CustomTabBarModel(null, LocaleKeys.status.tr(context: context)), //TODO: Need to be Disabled This Tab For Now diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart index 379540a3..38f0f70b 100644 --- a/lib/presentation/contact_us/find_us_page.dart +++ b/lib/presentation/contact_us/find_us_page.dart @@ -44,7 +44,7 @@ class FindUsPage extends StatelessWidget { ? SizedBox.shrink() : CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.hmgHospitals.tr()), CustomTabBarModel(null, LocaleKeys.hmgPharmacy.tr()), diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 582fd6fc..42669a71 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -667,7 +667,7 @@ class _ServicesPageState extends State { borderWidth: 0.w, isBold: true, borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.08), padding: EdgeInsets.all(8.w), fontSize: 14.f, onPressed: () { diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index fdb8afcf..b9c9c245 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; @@ -49,6 +50,7 @@ import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dar import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/lakum_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; +import 'package:hmg_patient_app_new/presentation/home/widgets/occasion_banner_widget.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; @@ -232,6 +234,11 @@ class _LandingPageState extends State { RefreshIndicator( color: AppColors.primaryRedColor, onRefresh: () async { + // Re-check Remote Config for occasion theme changes on + // pull-to-refresh (independent of auth state) — handy for + // testing without needing to restart the app. + unawaited(GetIt.instance().refresh()); + if (appState.isAuthenticated) { // Refresh Appointments Data myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); @@ -282,6 +289,9 @@ class _LandingPageState extends State { child: Column( spacing: 16.h, children: [ + // Renders nothing (zero height) unless a remote-config + // occasion theme (e.g. National Day) is currently active. + const OccasionBannerWidget(), Row( spacing: 8.h, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -715,7 +725,7 @@ class _LandingPageState extends State { // // children: [ // // AppCustomChipWidget( // // labelText: LocaleKeys.erOnlineCheckInRequest.tr(context: context), - // // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), + // // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.10), // // textColor: AppColors.primaryRedColor, // // ), // // Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), @@ -1235,7 +1245,7 @@ class _LandingPageState extends State { iconColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, textColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, - backgroundColor: isVitalCall ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), + backgroundColor: isVitalCall ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only( start: 8.h, end: -2.h, diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart new file mode 100644 index 00000000..38958426 --- /dev/null +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -0,0 +1,61 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Renders the remotely-configured occasion imagery: +/// - `background_opacity_image`: a decorative backdrop/watermark drawn at +/// [OccasionThemeViewModel.backgroundImageOpacity]. +/// - `header_image`: the occasion's main header/banner (e.g. the National +/// Day greeting banner), drawn on top at full opacity. +/// +/// Fully data-driven: when no occasion is active (the default), this widget +/// renders nothing and takes zero space — the existing layout/UI is +/// completely unaffected. +class OccasionBannerWidget extends StatelessWidget { + const OccasionBannerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, occasionThemeVm, _) { + final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; + final headerImageUrl = occasionThemeVm.headerImage; + final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); + final hasHeaderImage = occasionThemeVm.isOccasionActive && (headerImageUrl?.isNotEmpty ?? false); + + if (!hasBackgroundImage && !hasHeaderImage) { + return const SizedBox.shrink(); + } + + return Stack( + alignment: Alignment.center, + children: [ + if (hasBackgroundImage) + Opacity( + opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), + child: CachedNetworkImage( + imageUrl: backgroundImageUrl!, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ), + if (hasHeaderImage) + CachedNetworkImage( + imageUrl: headerImageUrl!, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ], + ); + }, + ); + } +} + + + diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 1a5fe039..5b23bab2 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -58,7 +58,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { ? AppColors.successColor.withValues(alpha: 0.1) : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) ? AppColors.warningColorYellow.withValues(alpha: 0.1) - : AppColors.primaryRedColor.withValues(alpha: 0.1), + : AppColors.primaryRedColorBase.withValues(alpha: 0.1), textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index 4bc98627..23b14db9 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -37,7 +37,7 @@ class InsuranceApprovalCard extends StatelessWidget { } else if (_isPartiallyApproved()) { return AppColors.warningColorYellow.withValues(alpha: 0.1); } else { - return AppColors.primaryRedColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } } @@ -126,7 +126,7 @@ class InsuranceApprovalCard extends StatelessWidget { runSpacing: 4.h, children: [ // AppCustomChipWidget(labelText: isLoading ? "Approved" : insuranceApprovalResponseModel.approvalStatusDescption!, - // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColor.withValues(alpha: 0.1), + // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColorBase.withValues(alpha: 0.1), // textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, // ).toShimmer2(isShow: isLoading), AppCustomChipWidget(labelText: isLoading ? "Cardiology" : insuranceApprovalResponseModel.clinicName ?? "").toShimmer2(isShow: isLoading), diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 0f5bde1e..2393e875 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -69,8 +69,8 @@ class InsuranceHistory extends StatelessWidget { CustomButton( text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription ?? "", onPressed: () {}, - backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - borderColor: AppColors.primaryRedColor.withOpacity(0.0), + backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), + borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), textColor: AppColors.primaryRedColor, fontSize: 10, isBold: true, diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 6fab990c..4b352474 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -117,7 +117,7 @@ class PatientInsuranceCard extends StatelessWidget { } }, backgroundColor: isCurrentPatientInsuranceExpired(insuranceCardDetailsModel.cardValidTo!) - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : insuranceViewModel.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), @@ -129,7 +129,7 @@ class PatientInsuranceCard extends StatelessWidget { // iconColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // textColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // iconSize: 12, - // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), // labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.h), // ), ], diff --git a/lib/presentation/medical_file/eye_measurement_details_page.dart b/lib/presentation/medical_file/eye_measurement_details_page.dart index 68f261cd..90dbba10 100644 --- a/lib/presentation/medical_file/eye_measurement_details_page.dart +++ b/lib/presentation/medical_file/eye_measurement_details_page.dart @@ -47,7 +47,7 @@ class EyeMeasurementDetailsPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.classes.tr()), CustomTabBarModel(null, LocaleKeys.contactLens.tr()), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 27978723..99a6db10 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -340,7 +340,7 @@ class _MedicalFilePageState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 8feb8434..16130d0c 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -198,7 +198,7 @@ class _FamilyCardsState extends State { backgroundColor: profile.status == FamilyFileEnum.pending.toInt ? AppColors.alertLightColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.active.toInt ? AppColors.lightGreenColor : AppColors.lightGrayBGColor, @@ -410,7 +410,7 @@ class _FamilyCardsState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.warningColorYellow.withValues(alpha: 0.1), @@ -575,7 +575,7 @@ class _FamilyCardsState extends State { backgroundColor: profile.status == FamilyFileEnum.pending.toInt ? AppColors.alertLightColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.active.toInt ? AppColors.lightGreenColor : AppColors.lightGrayBGColor, diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 8fea72fc..5f1b3d3a 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -201,8 +201,8 @@ class InvoiceListCard extends StatelessWidget { onTap!(); } }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, @@ -223,7 +223,7 @@ class InvoiceListCard extends StatelessWidget { // icon: AppAssets.refundIcon, // iconColor: AppColors.caloriesCalculatorColor, // backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), - // borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + // borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), // textColor: AppColors.caloriesCalculatorColor, // fontSize: 14.f, // fontWeight: FontWeight.w600, diff --git a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart index 2aaefcac..4c8be01e 100644 --- a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart +++ b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart @@ -57,7 +57,7 @@ class PharmacyInvoiceCard extends StatelessWidget { // if (pharmacyInvoice.totalAmount != null) // AppCustomChipWidget( // labelText: '${pharmacyInvoice.totalAmount!.toStringAsFixed(2)} ${LocaleKeys.sar.tr(context: context)}', - // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), // textColor: AppColors.primaryRedColor, // isEnglishOnly: true, // ), @@ -158,8 +158,8 @@ class PharmacyInvoiceCard extends StatelessWidget { ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context), onPressed: onTap, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index b0e11af0..59b10620 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -177,8 +177,8 @@ class PrescriptionItemView extends StatelessWidget { // child: CustomButton( // text: LocaleKeys.checkAvailability.tr(context: context), // onPressed: () {}, - // backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - // borderColor: AppColors.primaryRedColor.withOpacity(0.0), + // backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), + // borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), // textColor: AppColors.primaryRedColor, // fontSize: 14.f, // isBold: true, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 2e9bb3b7..af6623c8 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -701,7 +701,7 @@ class _FamilyCardWidgetState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.warningColorYellow.withValues(alpha: 0.1), diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index fbd1d58b..92729d8a 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -123,7 +123,7 @@ class FamilyCardWidget extends StatelessWidget { icon = AppAssets.cancel_circle_icon; labelText = LocaleKeys.insuranceExpired.tr(context: context); iconColor = AppColors.primaryRedColor; - backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); + backgroundColor = AppColors.primaryRedColorBase.withValues(alpha: 0.15); } else { icon = AppAssets.insurance_active_icon; labelText = LocaleKeys.insuranceActive.tr(context: context); diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 630c5dc0..d68b7494 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -173,8 +173,8 @@ class _RefundHistoryScreenState extends State { CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))), - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 0e150733..c9f07c96 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -289,7 +289,7 @@ class _RiskFactorsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index 5e102581..e49f0521 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -268,7 +268,7 @@ class _SuggestionsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 6a83316b..2e57664a 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -360,7 +360,7 @@ class _SymptomsSelectorPageState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 95de9464..24a8ddcd 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -750,7 +750,7 @@ class _TriagePageState extends State { percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( @@ -785,7 +785,7 @@ class _TriagePageState extends State { text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, isDisabled: viewModel.isTriageDiagnosisLoading, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart index 6672a968..3f9c5747 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -148,7 +148,7 @@ class _UserInfoFlowManagerState extends State { boxShadow: isCurrentStep && value > 0 ? [ BoxShadow( - color: AppColors.primaryRedColor.withValues(alpha: 0.4), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.4), blurRadius: 8.r, spreadRadius: 1.r, ), @@ -191,8 +191,8 @@ class _UserInfoFlowManagerState extends State { isDisabled: !(viewModel.isCurrentUserInfoPageValid), onPressed: () => viewModel.isCurrentUserInfoPageValid ? _onNext() : () {}, backgroundColor: - viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), - borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), + viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), + borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), textColor: AppColors.whiteColor, fontSize: 16.f, ) @@ -204,7 +204,7 @@ class _UserInfoFlowManagerState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPrevious, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection_screen.dart b/lib/presentation/symptoms_checker/user_info_selection_screen.dart index 589771ec..fef576dc 100644 --- a/lib/presentation/symptoms_checker/user_info_selection_screen.dart +++ b/lib/presentation/symptoms_checker/user_info_selection_screen.dart @@ -296,7 +296,7 @@ class _UserInfoSelectionScreenState extends State { context.read().setUserInfoPage(0, isSinglePageEdit: false); context.navigateWithName(AppRoutes.userInfoFlowManager); }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart index 307445a4..073aa110 100644 --- a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart +++ b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart @@ -188,7 +188,7 @@ class _PinchZoomTutorialOverlayState extends State wit width: 48.w, height: 48.h, decoration: BoxDecoration( - color: AppColors.primaryRedColor.withValues(alpha: 0.1), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), ), child: Icon( diff --git a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart index 19ad3935..b1e8beea 100644 --- a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart +++ b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart @@ -94,7 +94,7 @@ class _TriageLevelCardState extends State with SingleTickerProv switch (triageLevel) { case 'emergency_ambulance': case 'emergency': - return AppColors.chipColorEmergency.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); case 'consultation_24': case 'consultation': return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1); diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart index 058f14e8..2d61378b 100644 --- a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -91,7 +91,7 @@ class SwitchCupBottomSheet extends StatelessWidget { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent, + color: isSelected ? AppColors.primaryRedColorBase.withOpacity(0.08) : Colors.transparent, borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, @@ -271,9 +271,9 @@ class _CustomizeCupBottomSheetState extends State { SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.primaryRedColor, - inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2), + inactiveTrackColor: AppColors.primaryRedColorBase.withOpacity(0.2), thumbColor: AppColors.primaryRedColor, - overlayColor: AppColors.primaryRedColor.withOpacity(0.2), + overlayColor: AppColors.primaryRedColorBase.withOpacity(0.2), trackHeight: 4.h, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), ), diff --git a/lib/presentation/weather/weather_details_page.dart b/lib/presentation/weather/weather_details_page.dart index 8762a55a..7e93b9b6 100644 --- a/lib/presentation/weather/weather_details_page.dart +++ b/lib/presentation/weather/weather_details_page.dart @@ -62,7 +62,7 @@ class _WeatherDetailsPageState extends State { case ColorName.ORANGE: return AppColors.warningLightColor.withValues(alpha: 0.1); case ColorName.RED: - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } } @@ -120,7 +120,7 @@ class _WeatherDetailsPageState extends State { } // Red: Very High, At High Risk else if (category.contains('very high') || category.contains('at high risk')) { - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } // Orange: Risk, High (but not "very high" or "at high risk") else if (category.contains('risk') || category.contains('high')) { diff --git a/lib/services/remote_config_service.dart b/lib/services/remote_config_service.dart new file mode 100644 index 00000000..e8a7ec16 --- /dev/null +++ b/lib/services/remote_config_service.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/services.dart' show rootBundle; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Firebase Remote Config wrapper. Fully generic — the app never hardcodes +/// occasion-specific values here; it only knows about the `occasion_config` +/// remote key and reads whatever JSON is published on the Firebase console. +abstract class RemoteConfigService { + /// Must be called once at startup, before the first frame if possible. + Future initialize(); + + /// Fetches the latest values from Firebase (respecting the configured + /// minimum fetch interval) and activates them. + Future fetchAndActivate(); + + /// Raw JSON string currently held for the `occasion_config` key + /// (bundled default until the first successful fetch/activate). + String get occasionConfigRawJson; +} + +class RemoteConfigServiceImp implements RemoteConfigService { + static const String occasionConfigKey = 'occasion_config'; + static const String _defaultConfigAssetPath = 'assets/json/default_occasion_config.json'; + + final LoggerService loggerService; + final FirebaseRemoteConfig _remoteConfig; + + RemoteConfigServiceImp({ + required this.loggerService, + FirebaseRemoteConfig? remoteConfig, + }) : _remoteConfig = remoteConfig ?? FirebaseRemoteConfig.instance; + + @override + Future initialize() async { + try { + await _remoteConfig.setConfigSettings( + RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 10), + // In debug builds, always fetch fresh values so you can test + // Remote Config changes instantly without waiting/throttling. + // Release builds keep a sane 1-hour minimum interval. + minimumFetchInterval: kDebugMode ? Duration.zero : const Duration(hours: 1), + ), + ); + + // Bundled fallback so behaviour is deterministic (and identical to the + // current app look) before the very first successful remote fetch. + final defaultJson = await rootBundle.loadString(_defaultConfigAssetPath); + await _remoteConfig.setDefaults({occasionConfigKey: defaultJson}); + } catch (ex) { + loggerService.logError('RemoteConfigService.initialize error: $ex'); + } + } + + @override + Future fetchAndActivate() async { + try { + return await _remoteConfig.fetchAndActivate(); + } catch (ex) { + loggerService.logError('RemoteConfigService.fetchAndActivate error: $ex'); + return false; + } + } + + @override + String get occasionConfigRawJson { + try { + final value = _remoteConfig.getString(occasionConfigKey); + if (value.isEmpty) return _fallbackDisabledJson; + // Validate JSON shape early; if malformed, fall back safely. + json.decode(value); + return value; + } catch (ex) { + loggerService.logError('RemoteConfigService.occasionConfigRawJson parse error: $ex'); + return _fallbackDisabledJson; + } + } + + static const String _fallbackDisabledJson = '{"key":"none","enabled":false}'; +} + diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 1ecbcb61..2be811d8 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -5,6 +5,15 @@ class AppColors { /// When true, every getter below returns the dark palette value. static bool isDarkMode = false; + /// Global occasion-theme flag — toggled by OccasionThemeViewModel based on + /// Firebase Remote Config. When false (the default), the app looks exactly + /// like it does today — no colors are affected. + static bool isOccasionThemeActive = false; + + /// Remotely-configured primary color for the currently active occasion + /// (e.g. National Day). Null unless an occasion theme is active. + static Color? occasionPrimaryColor; + static const transparent = Colors.transparent; // ── Scaffold / Background ───────────────────────────────────────────────── @@ -20,7 +29,20 @@ class AppColors { // ── Core Colors ─────────────────────────────────────────────────────────── static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFffffff); // static const Color primaryRedColor = Color(0xFFED1C2B); - static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : Color(0xFFED1C2B); + /// Occasion-aware primary brand color. Resolves to the remotely-configured + /// occasion color when active (e.g. National Day green), otherwise falls + /// back to the original dark/light red — identical to current behavior. + /// + /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived + /// (translucent) colors — use [primaryRedColorBase] for those instead, so + /// occasion theming never affects translucent overlays/backgrounds. + static Color get primaryRedColor => + (isOccasionThemeActive && occasionPrimaryColor != null) ? occasionPrimaryColor! : primaryRedColorBase; + + /// The original, non-themed brand red (or its dark-mode variant). Always + /// stable regardless of any active occasion theme — intended for opacity + /// derived usages (e.g. `AppColors.primaryRedColorBase.withOpacity(0.1)`). + static Color get primaryRedColorBase => isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B); static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); static const Color successColor = Color(0xFF18C273); static Color get successLightBgColor => isDarkMode ? dark.successLightBgColor : const Color(0xffDDF6EA); diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index e999842f..a5b8b239 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -28,7 +28,13 @@ class BottomNavigation extends StatelessWidget { icon: AppAssets.bookAppoBottom, fillIcon: AppAssets.bookAppoBottom, label: LocaleKeys.appointments.tr(context: context), - iconSize: 32, isSpecial: true), + iconSize: 32, + isSpecial: true, + // Explicit dynamic color — the SVG asset itself is a fixed red, so + // without this override the Appointments icon would never react to + // dark mode or the active occasion theme (e.g. National Day green). + color: AppColors.primaryRedColor, + ), appState.isAuthenticated // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, fillIcon: AppAssets.symptomCheckerBottomFillIcon, label: LocaleKeys.symptoms.tr(context: context)) @@ -75,7 +81,8 @@ class BottomNavigation extends StatelessWidget { icon: isSelected ? item.fillIcon : item.icon, height: item.iconSize.h, width: item.iconSize.w, - applyThemeColor: !item.isSpecial + applyThemeColor: !item.isSpecial, + iconColor: item.color, // iconColor: isSelected ? Colors.black87 : Colors.black87, ), // const SizedBox(height: 10), diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index a544f010..60178939 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -163,7 +163,7 @@ class _DateRangeSelectorState extends State { ), startRangeSelectionColor: AppColors.primaryRedColor, endRangeSelectionColor: AppColors.primaryRedColor, - rangeSelectionColor: AppColors.primaryRedColor.withOpacity(0.1), + rangeSelectionColor: AppColors.primaryRedColorBase.withOpacity(0.1), rangeTextStyle: TextStyle( fontFamily: "Poppins", ), @@ -963,7 +963,7 @@ class _DateRangeSelectorState extends State { children: [ AppCustomChipWidget( labelText: LocaleKeys.thisWeek.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -977,7 +977,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastMonth.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -991,7 +991,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastSixMonths.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -1012,7 +1012,7 @@ class _DateRangeSelectorState extends State { ], ), labelText: "${LocaleKeys.year.tr(context: context)} ${model.getCurrentYear}", - backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index 50da5f3a..2994db57 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -220,7 +220,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/lib/widgets/map/gms_map.dart b/lib/widgets/map/gms_map.dart index 33e0befb..1fe7f442 100644 --- a/lib/widgets/map/gms_map.dart +++ b/lib/widgets/map/gms_map.dart @@ -372,7 +372,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/pubspec.yaml b/pubspec.yaml index 82ae76ac..9e9977c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: google_api_availability: ^5.0.1 firebase_analytics: ^11.5.1 firebase_crashlytics: ^4.3.8 + firebase_remote_config: ^5.5.0 jiffy: ^6.4.3 # hijri_gregorian_calendar: # path: /Users/aamir/StudioProjects/hijri_gregorian_calendar From fe9d76affd8b65b3c226990f85673d09aad517d2 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Tue, 22 Sep 2026 15:14:35 +0300 Subject: [PATCH 70/73] Completed THE Theme changes --- assets/json/default_occasion_config.json | 1 + ios/Flutter/AppFrameworkInfo.plist | 2 - ios/Runner.xcodeproj/project.pbxproj | 22 ++ .../xcshareddata/xcschemes/Runner.xcscheme | 18 + lib/core/api_consts.dart | 2 +- lib/core/utils/utils.dart | 8 +- .../occasion_theme/occasion_config_model.dart | 6 + .../occasion_theme_view_model.dart | 7 + .../appointments/appointment_queue_page.dart | 2 +- .../widgets/ask_doctor_appointment_card.dart | 2 +- .../authentication/quick_login.dart | 31 +- .../authentication/saved_login_screen.dart | 12 +- .../blood_donation/blood_donation_page.dart | 4 +- .../contact_us/feedback_page.dart | 2 +- lib/presentation/contact_us/find_us_page.dart | 2 +- .../hmg_services/services_page.dart | 2 +- .../home/data/landing_page_data.dart | 366 +++++++++--------- lib/presentation/home/landing_page.dart | 29 +- .../home/widgets/occasion_banner_widget.dart | 105 +++-- .../home/widgets/small_service_card.dart | 2 +- .../insurance_approval_details_page.dart | 2 +- .../widgets/insurance_approval_card.dart | 4 +- .../insurance/widgets/insurance_history.dart | 4 +- .../widgets/patient_insurance_card.dart | 2 +- .../eye_measurement_details_page.dart | 2 +- .../medical_file/medical_file_page.dart | 2 +- lib/presentation/my_family/my_family.dart | 10 +- .../my_family/widget/family_cards.dart | 124 +++--- .../widgets/invoice_list_card.dart | 6 +- .../widgets/pharmacy_invoice_card.dart | 6 +- .../profile_settings/profile_settings.dart | 171 ++++---- .../widgets/family_card_widget.dart | 2 +- .../refund_request/refund_history_screen.dart | 4 +- .../organ_selector_screen.dart | 3 +- .../symptoms_checker/risk_factors_screen.dart | 2 +- .../symptoms_checker/suggestions_screen.dart | 2 +- .../symptoms_selector_screen.dart | 2 +- .../symptoms_checker/triage_screen.dart | 4 +- .../user_info_flow_manager.dart | 8 +- .../user_info_selection_screen.dart | 2 +- .../widgets/pinch_zoom_tutorial_overlay.dart | 2 +- .../widgets/selected_organs_section.dart | 2 +- .../widgets/triage_level_card.dart | 2 +- .../widgets/cup_bottomsheet_widgets.dart | 6 +- .../weather/weather_details_page.dart | 4 +- lib/services/remote_config_service.dart | 1 - lib/theme/colors.dart | 243 +++++++++++- lib/widgets/appbar/collapsing_list_view.dart | 52 +-- .../date_range_calender.dart | 10 +- lib/widgets/language_switcher.dart | 15 +- lib/widgets/map/HMSMap.dart | 2 +- lib/widgets/map/gms_map.dart | 2 +- pubspec.lock | 44 ++- 53 files changed, 861 insertions(+), 511 deletions(-) diff --git a/assets/json/default_occasion_config.json b/assets/json/default_occasion_config.json index 37740e39..6b38a4cb 100644 --- a/assets/json/default_occasion_config.json +++ b/assets/json/default_occasion_config.json @@ -4,6 +4,7 @@ "start_date": null, "end_date": null, "primary_color": null, + "secondary_color": null, "background_opacity_image": null, "background_image_opacity": 0.2, "header_image": null diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 21dccceb..dac5fbe9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -90,6 +91,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,6 +99,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 76B1D2512F91BF8000DBBD1B /* Penguin.xcframework in Frameworks */, 76B1D2552F91BF8000DBBD1B /* PenNavUI.xcframework in Frameworks */, 76B1D2532F91BF8000DBBD1B /* PenguinINRenderer.xcframework in Frameworks */, @@ -159,6 +162,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -230,6 +234,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -256,6 +263,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -790,6 +800,18 @@ defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42..c3fedb29 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + _hexToColor(primaryColorHex); + /// Lighter/translucent variant of [primaryColor] — 11% opacity of the same + /// hue (e.g. for chip/badge backgrounds). Derived automatically from + /// [primaryColor]; not an independent remote-config field. Null unless + /// [primaryColor] is configured. + Color? get secondaryColor => primaryColor?.withValues(alpha: 0.11); + static Color? _hexToColor(String? hex) { if (hex == null || hex.isEmpty) return null; var value = hex.trim().replaceFirst('#', ''); diff --git a/lib/features/occasion_theme/occasion_theme_view_model.dart b/lib/features/occasion_theme/occasion_theme_view_model.dart index b1016589..9dd22b09 100644 --- a/lib/features/occasion_theme/occasion_theme_view_model.dart +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart' show Color; import 'package:hmg_patient_app_new/features/occasion_theme/occasion_config_model.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -45,6 +46,11 @@ class OccasionThemeViewModel extends ChangeNotifier { /// banner), rendered at full opacity. Null unless an occasion is active. String? get headerImage => isOccasionActive ? _config.headerImage : null; + /// Lighter/translucent variant of the active occasion's primary color + /// (11% opacity) — e.g. for chip/badge backgrounds. Null unless an + /// occasion is active. + Color? get secondaryColor => isOccasionActive ? _config.secondaryColor : null; + /// Call once at app startup (before the first frame) to restore the last /// known-good config instantly (offline-safe), then kick off a remote /// fetch in the background. @@ -97,6 +103,7 @@ class OccasionThemeViewModel extends ChangeNotifier { void _applyToAppColors() { AppColors.isOccasionThemeActive = isOccasionActive; AppColors.occasionPrimaryColor = _config.primaryColor; + AppColors.occasionSecondaryColor = _config.secondaryColor; } } diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart index 031295e5..f2bd1d18 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -150,7 +150,7 @@ class AppointmentQueuePage extends StatelessWidget { textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 - ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) + ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), ), 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 bd9fdef9..e7d7c187 100644 --- a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart +++ b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart @@ -68,7 +68,7 @@ class AskDoctorAppointmentCard extends StatelessWidget { ), AppCustomChipWidget( labelText: LocaleKeys.outPatient.tr(context: context), - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), textColor: AppColors.primaryRedColor, ), AppCustomChipWidget( diff --git a/lib/presentation/authentication/quick_login.dart b/lib/presentation/authentication/quick_login.dart index 133e38ef..0830b23e 100644 --- a/lib/presentation/authentication/quick_login.dart +++ b/lib/presentation/authentication/quick_login.dart @@ -94,17 +94,18 @@ class QuickLoginState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( - child: CustomButton( - text: LocaleKeys.enableQuickLoginBtn.tr(context: context), - onPressed: () { - widget.onPressed(); - }, - backgroundColor: Color(0xffED1C2B), - borderColor: Color(0xffED1C2B), - textColor: Colors.white, - icon: AppAssets.apple_finder, - height: 56.h, - )), + child: CustomButton( + text: LocaleKeys.enableQuickLoginBtn.tr(context: context), + onPressed: () { + widget.onPressed(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + icon: AppAssets.apple_finder, + height: 56.h, + ), + ), ], ), SizedBox( @@ -113,16 +114,16 @@ class QuickLoginState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Expanded( + Expanded( child: CustomButton( text: LocaleKeys.notNow.tr(context: context), onPressed: () { cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: false); Navigator.pop(context, "true"); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Colors.red, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, height: 56.h, // icon: "assets/images/svg/apple-finder.svg", )), diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index bea93d36..4de4bb64 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -327,9 +327,9 @@ class _SavedLogin extends State with RouteAware { // MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()) // ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, fontSize: 16.f, fontWeight: FontWeight.w600, borderRadius: 12.r, @@ -352,9 +352,9 @@ class _SavedLogin extends State with RouteAware { MaterialPageRoute(builder: (BuildContext context) => LoginScreen()), ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, fontSize: 15.f, fontWeight: FontWeight.w600, borderRadius: 12.r, diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart index 62c409c6..b4bdb6df 100644 --- a/lib/presentation/blood_donation/blood_donation_page.dart +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -232,7 +232,7 @@ class _BloodDonationPageState extends State { Container( padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.bgRedLightColor.withValues(alpha: 0.1), + color: AppColors.secondaryLightRedColorBase.withValues(alpha: 0.1), borderRadius: 12.r, hasShadow: false, ), @@ -244,7 +244,7 @@ class _BloodDonationPageState extends State { Container( padding: EdgeInsets.all(8.h), decoration: BoxDecoration( - color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + color: AppColors.primaryRedColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8.r), ), child: Utils.buildSvgWithAssets( diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index a5d68614..91fa3b52 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -59,7 +59,7 @@ class FeedbackPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.send.tr(context: context)), CustomTabBarModel(null, LocaleKeys.status.tr(context: context)), //TODO: Need to be Disabled This Tab For Now diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart index 38f0f70b..379540a3 100644 --- a/lib/presentation/contact_us/find_us_page.dart +++ b/lib/presentation/contact_us/find_us_page.dart @@ -44,7 +44,7 @@ class FindUsPage extends StatelessWidget { ? SizedBox.shrink() : CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.hmgHospitals.tr()), CustomTabBarModel(null, LocaleKeys.hmgPharmacy.tr()), diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 42669a71..582fd6fc 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -667,7 +667,7 @@ class _ServicesPageState extends State { borderWidth: 0.w, isBold: true, borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.08), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), padding: EdgeInsets.all(8.w), fontSize: 14.f, onPressed: () { diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index 3d98e5a1..026c1725 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -9,192 +9,190 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; class LandingPageData { static List get getNotLoggedInServiceCardsList => [ - ServiceCardData( - serviceName: "emergency", - icon: AppAssets.emergency_services_icon, - title: getIt.get().isArabic()?LocaleKeys.services3 :LocaleKeys.emergency2, - subtitle: getIt.get().isArabic()?LocaleKeys.emergency2:LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColor, - iconColor: Colors.white, - textColor: Colors.white, - isBold: true, - ), - ServiceCardData( - serviceName: "search_doctor", - icon: AppAssets.search_doctor_icon, - title: LocaleKeys.finda, - subtitle: LocaleKeys.doctor, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "health_calculators_and_converts", - icon: AppAssets.health_calculators_icon, - title: getIt.get().isArabic()? LocaleKeys.calculators:LocaleKeys.health , - subtitle: getIt.get().isArabic()? LocaleKeys.health:LocaleKeys.calculators, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "health_converters", - icon: AppAssets.health_converters_icon, - title: getIt.get().isArabic()? LocaleKeys.converters:LocaleKeys.health, - subtitle: getIt.get().isArabic()? LocaleKeys.health:LocaleKeys.converters, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - // ServiceCardData( - // serviceName: "parking_guide", - // icon: AppAssets.car_parking_icon, - // title: getIt.get().isArabic()? LocaleKeys.guide:LocaleKeys.parking2, - // subtitle: getIt.get().isArabic()? LocaleKeys.parking2:LocaleKeys.guide, - // backgroundColor: AppColors.whiteColor, - // iconColor: AppColors.blackColor, - // textColor: AppColors.blackColor, - // isBold: false, - // ), - ServiceCardData( - serviceName: "indoor_navigation", - icon: AppAssets.indoor_nav_icon, - title: LocaleKeys.indoor, - subtitle: LocaleKeys.thehospital, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ]; + ServiceCardData( + serviceName: "emergency", + icon: AppAssets.emergency_services_icon, + title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, + subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, + backgroundColor: AppColors.primaryRedColorBase, + iconColor: Colors.white, + textColor: Colors.white, + isBold: true, + ), + ServiceCardData( + serviceName: "search_doctor", + icon: AppAssets.search_doctor_icon, + title: LocaleKeys.finda, + subtitle: LocaleKeys.doctor, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "health_calculators_and_converts", + icon: AppAssets.health_calculators_icon, + title: getIt.get().isArabic() ? LocaleKeys.calculators : LocaleKeys.health, + subtitle: getIt.get().isArabic() ? LocaleKeys.health : LocaleKeys.calculators, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "health_converters", + icon: AppAssets.health_converters_icon, + title: getIt.get().isArabic() ? LocaleKeys.converters : LocaleKeys.health, + subtitle: getIt.get().isArabic() ? LocaleKeys.health : LocaleKeys.converters, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + // ServiceCardData( + // serviceName: "parking_guide", + // icon: AppAssets.car_parking_icon, + // title: getIt.get().isArabic()? LocaleKeys.guide:LocaleKeys.parking2, + // subtitle: getIt.get().isArabic()? LocaleKeys.parking2:LocaleKeys.guide, + // backgroundColor: AppColors.whiteColor, + // iconColor: AppColors.blackColor, + // textColor: AppColors.blackColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "indoor_navigation", + icon: AppAssets.indoor_nav_icon, + title: LocaleKeys.indoor, + subtitle: LocaleKeys.thehospital, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ]; List get getLoggedInServiceCardsList => [ - ServiceCardData( - serviceName: "emergency", - icon: AppAssets.emergency_services_icon, - title: getIt.get().isArabic()?LocaleKeys.services3 :LocaleKeys.emergency2, - subtitle: getIt.get().isArabic()?LocaleKeys.emergency2:LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColor, - iconColor: Colors.white, - textColor: Colors.white, - isBold: true, - ), - ServiceCardData( - serviceName: "lab_results", - icon: AppAssets.home_lab_result_icon, - title: getIt.get().isArabic()?LocaleKeys.labSubtitle:LocaleKeys.lab, - subtitle: getIt.get().isArabic()?LocaleKeys.lab:LocaleKeys.labSubtitle , - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "radiology_results", - icon: AppAssets.my_radiology_icon, - title: getIt.get().isArabic()?LocaleKeys.labSubtitle:LocaleKeys.radiology, - subtitle: getIt.get().isArabic()?LocaleKeys.radiology:LocaleKeys.labSubtitle , - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "prescriptions", - icon: AppAssets.my_prescription_icon, - title: getIt.get().isArabic()?LocaleKeys.medicinesSubtitle:LocaleKeys.medicalKey, - subtitle: getIt.get().isArabic()?LocaleKeys.medicalKey:LocaleKeys.medicinesSubtitle, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "insurance_update", - icon: AppAssets.insurance_update_icon, - title: getIt.get().isArabic()?LocaleKeys.insurance :LocaleKeys.insurance, - subtitle: getIt.get().isArabic()?LocaleKeys.medical: LocaleKeys.details2, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "my_doctors", - icon: AppAssets.my_doctors_icon, - title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2.tr() : "My", - subtitle: getIt.get().isArabic() ? LocaleKeys.myDoctor.tr() : "Doctors", - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "sick_leaves", - icon: AppAssets.my_sick_leave_icon, - title: getIt.get().isArabic()?LocaleKeys.sickSubtitle:LocaleKeys.sick2, - subtitle: getIt.get().isArabic()?LocaleKeys.sick2:LocaleKeys.sickSubtitle, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "my_invoice", - icon: AppAssets.invoiceListIcon, - title: getIt.get().isArabic()?LocaleKeys.myDoctorSubtitle2:LocaleKeys.invoiceList2, - subtitle: getIt.get().isArabic()?LocaleKeys.invoiceList2:LocaleKeys.myDoctorSubtitle2, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ]; + ServiceCardData( + serviceName: "emergency", + icon: AppAssets.emergency_services_icon, + title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, + subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, + backgroundColor: AppColors.primaryRedColorBase, + iconColor: Colors.white, + textColor: Colors.white, + isBold: true, + ), + ServiceCardData( + serviceName: "lab_results", + icon: AppAssets.home_lab_result_icon, + title: getIt.get().isArabic() ? LocaleKeys.labSubtitle : LocaleKeys.lab, + subtitle: getIt.get().isArabic() ? LocaleKeys.lab : LocaleKeys.labSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "radiology_results", + icon: AppAssets.my_radiology_icon, + title: getIt.get().isArabic() ? LocaleKeys.labSubtitle : LocaleKeys.radiology, + subtitle: getIt.get().isArabic() ? LocaleKeys.radiology : LocaleKeys.labSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "prescriptions", + icon: AppAssets.my_prescription_icon, + title: getIt.get().isArabic() ? LocaleKeys.medicinesSubtitle : LocaleKeys.medicalKey, + subtitle: getIt.get().isArabic() ? LocaleKeys.medicalKey : LocaleKeys.medicinesSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "insurance_update", + icon: AppAssets.insurance_update_icon, + title: getIt.get().isArabic() ? LocaleKeys.insurance : LocaleKeys.insurance, + subtitle: getIt.get().isArabic() ? LocaleKeys.medical : LocaleKeys.details2, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "my_doctors", + icon: AppAssets.my_doctors_icon, + title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2.tr() : "My", + subtitle: getIt.get().isArabic() ? LocaleKeys.myDoctor.tr() : "Doctors", + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "sick_leaves", + icon: AppAssets.my_sick_leave_icon, + title: getIt.get().isArabic() ? LocaleKeys.sickSubtitle : LocaleKeys.sick2, + subtitle: getIt.get().isArabic() ? LocaleKeys.sick2 : LocaleKeys.sickSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "my_invoice", + icon: AppAssets.invoiceListIcon, + title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2 : LocaleKeys.invoiceList2, + subtitle: getIt.get().isArabic() ? LocaleKeys.invoiceList2 : LocaleKeys.myDoctorSubtitle2, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ]; static List get getServiceCardsList => [ - ServiceCardData( - serviceName: "livecare", - icon: AppAssets.small_livecare_icon, - title: LocaleKeys.liveCare, - subtitle: LocaleKeys.liveCareServiceDesc, - largeCardIcon: AppAssets.liveCareService, - backgroundColor: AppColors.successColor, - iconColor: AppColors.whiteColor, - isBold: false, - isPNG: false - ), - // ServiceCardData( - // icon: AppAssets.homeBottom, - // title: "Dermatology", - // subtitle: "Explore our app, View our services and offers", - // largeCardIcon: AppAssets.homeBottom, - // backgroundColor: AppColors.primaryRedColor, - // isBold: false, - // ), - ServiceCardData( - serviceName: "home_health_care", - icon: AppAssets.homeBottom, - title: LocaleKeys.homeHealthCare, - subtitle: LocaleKeys.homeHealthCareText, - largeCardIcon: AppAssets.homeHealthCareService, - backgroundColor: AppColors.primaryRedColor, - iconColor: AppColors.whiteColor, - isBold: false, - isPNG: false - ), - ServiceCardData( - serviceName: "pharmacy", - icon: AppAssets.defaultMEP, //359846 - title: LocaleKeys.hmgPharmacy, - subtitle: LocaleKeys.hmgPharmacyText, - largeCardIcon: AppAssets.pharmacyService, - backgroundColor: AppColors.pharmacyBGColor, - iconColor: null, - isBold: true, - isPNG: true - ), - ]; + ServiceCardData( + serviceName: "livecare", + icon: AppAssets.small_livecare_icon, + title: LocaleKeys.liveCare, + subtitle: LocaleKeys.liveCareServiceDesc, + largeCardIcon: AppAssets.liveCareService, + backgroundColor: AppColors.successColor, + iconColor: AppColors.whiteColor, + isBold: false, + isPNG: false), + // ServiceCardData( + // icon: AppAssets.homeBottom, + // title: "Dermatology", + // subtitle: "Explore our app, View our services and offers", + // largeCardIcon: AppAssets.homeBottom, + // backgroundColor: AppColors.primaryRedColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "home_health_care", + icon: AppAssets.homeBottom, + title: LocaleKeys.homeHealthCare, + subtitle: LocaleKeys.homeHealthCareText, + largeCardIcon: AppAssets.homeHealthCareService, + backgroundColor: AppColors.primaryRedColor, + iconColor: AppColors.whiteColor, + isBold: false, + isPNG: false), + ServiceCardData( + serviceName: "pharmacy", + icon: AppAssets.defaultMEP, + //359846 + title: LocaleKeys.hmgPharmacy, + subtitle: LocaleKeys.hmgPharmacyText, + largeCardIcon: AppAssets.pharmacyService, + backgroundColor: AppColors.pharmacyBGColor, + iconColor: null, + isBold: true, + isPNG: true), + ]; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b9c9c245..b7e537f1 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; import 'package:get_it/get_it.dart'; @@ -128,10 +129,7 @@ class _LandingPageState extends State { authVM.savePushTokenToAppState(); if (mounted) { final user = appState.getAuthenticatedUser(); - final hasUserQuickLoginData = - user != null && - (user.mobileNumber?.isNotEmpty ?? false) && - (user.patientIdentificationNo?.isNotEmpty ?? false); + final hasUserQuickLoginData = user != null && (user.mobileNumber?.isNotEmpty ?? false) && (user.patientIdentificationNo?.isNotEmpty ?? false); if (hasUserQuickLoginData) { authVM.checkLastLoginStatus(() { @@ -193,7 +191,7 @@ class _LandingPageState extends State { 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()); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr()); @@ -231,6 +229,11 @@ class _LandingPageState extends State { body: Consumer(builder: (context, insuranceVM, child) { return Stack( children: [ + // Renders nothing unless a remote-config occasion theme (e.g. + // National Day) is currently active — fills the whole page, + // behind all other content below. + const OccasionBackgroundWidget(), + RefreshIndicator( color: AppColors.primaryRedColor, onRefresh: () async { @@ -275,7 +278,6 @@ class _LandingPageState extends State { // } // }, // ); - } }, child: SingleChildScrollView( @@ -287,11 +289,9 @@ class _LandingPageState extends State { : kToolbarHeight + 16.h, bottom: 24.h), child: Column( - spacing: 16.h, children: [ - // Renders nothing (zero height) unless a remote-config - // occasion theme (e.g. National Day) is currently active. const OccasionBannerWidget(), + Row( spacing: 8.h, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -384,7 +384,7 @@ class _LandingPageState extends State { height: 18.h, padding: EdgeInsets.all(2), decoration: BoxDecoration( - color: AppColors.primaryRedColor, + color: AppColors.primaryRedColorBase, borderRadius: BorderRadius.circular(20.r), ), child: Text( @@ -450,6 +450,7 @@ class _LandingPageState extends State { }), ], ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), !appState.isAuthenticated ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -495,6 +496,7 @@ class _LandingPageState extends State { ), ).paddingSymmetrical(24.w, 0.h) : SizedBox.shrink(), + SizedBox(height: 16.h), // Offers And Discounts Carousel - Auto-scrolling from right to left // appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel @@ -725,7 +727,7 @@ class _LandingPageState extends State { // // children: [ // // AppCustomChipWidget( // // labelText: LocaleKeys.erOnlineCheckInRequest.tr(context: context), - // // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.10), + // // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), // // textColor: AppColors.primaryRedColor, // // ), // // Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), @@ -917,6 +919,7 @@ class _LandingPageState extends State { ], ), ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -932,6 +935,7 @@ class _LandingPageState extends State { }), ], ).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), ConstrainedBox( constraints: BoxConstraints(maxHeight: isFoldable ? 450.h : (isTablet ? 440.h : 431.h), minHeight: 411.h), child: ListView.separated( @@ -961,6 +965,7 @@ class _LandingPageState extends State { separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), ), + SizedBox(height: 16.h), appState.isAuthenticated ? Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1245,7 +1250,7 @@ class _LandingPageState extends State { iconColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, textColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, - backgroundColor: isVitalCall ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), + backgroundColor: isVitalCall ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only( start: 8.h, end: -2.h, diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart index 38958426..646cb70c 100644 --- a/lib/presentation/home/widgets/occasion_banner_widget.dart +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -1,17 +1,23 @@ +import 'dart:developer'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:provider/provider.dart'; -/// Renders the remotely-configured occasion imagery: -/// - `background_opacity_image`: a decorative backdrop/watermark drawn at -/// [OccasionThemeViewModel.backgroundImageOpacity]. -/// - `header_image`: the occasion's main header/banner (e.g. the National -/// Day greeting banner), drawn on top at full opacity. +/// Renders the occasion's main header/banner (`header_image`, e.g. the +/// National Day greeting banner) inline at the top of the landing page's +/// scrollable content, drawn at full opacity. /// /// Fully data-driven: when no occasion is active (the default), this widget /// renders nothing and takes zero space — the existing layout/UI is /// completely unaffected. +/// +/// See also [OccasionBackgroundWidget], which renders the decorative +/// backdrop image as a full-page background instead of inline content. class OccasionBannerWidget extends StatelessWidget { const OccasionBannerWidget({super.key}); @@ -19,43 +25,76 @@ class OccasionBannerWidget extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, occasionThemeVm, _) { - final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; final headerImageUrl = occasionThemeVm.headerImage; - final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); final hasHeaderImage = occasionThemeVm.isOccasionActive && (headerImageUrl?.isNotEmpty ?? false); - if (!hasBackgroundImage && !hasHeaderImage) { - return const SizedBox.shrink(); + if (!hasHeaderImage) { + return const SizedBox(height: 0); } - return Stack( - alignment: Alignment.center, - children: [ - if (hasBackgroundImage) - Opacity( - opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), - child: CachedNetworkImage( - imageUrl: backgroundImageUrl!, - width: double.infinity, - fit: BoxFit.cover, - placeholder: (_, __) => const SizedBox.shrink(), - errorWidget: (_, __, ___) => const SizedBox.shrink(), - ), - ), - if (hasHeaderImage) - CachedNetworkImage( - imageUrl: headerImageUrl!, - width: double.infinity, - fit: BoxFit.cover, - placeholder: (_, __) => const SizedBox.shrink(), - errorWidget: (_, __, ___) => const SizedBox.shrink(), - ), - ], - ); + return buildOccasionImage(headerImageUrl!).paddingOnly(bottom: 16.h); }, ); } } +/// Renders the occasion's decorative backdrop/watermark +/// (`background_opacity_image`) as a full-page background. Place this as +/// the first child of the landing page's outer [Stack] (behind the +/// scrollable content) so it fills the whole screen and sits behind +/// everything else, drawn at [OccasionThemeViewModel.backgroundImageOpacity]. +/// +/// Fully data-driven: when no occasion is active (the default), this widget +/// renders nothing — the existing layout/UI is completely unaffected. +class OccasionBackgroundWidget extends StatelessWidget { + const OccasionBackgroundWidget({super.key}); + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, occasionThemeVm, _) { + final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; + final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); + log("occasionThemeVm.backgroundImageOpacity: ${occasionThemeVm.backgroundImageOpacity}"); + + if (!hasBackgroundImage) { + return const SizedBox.shrink(); + } + + return Positioned.fill( + child: Opacity( + opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), + child: buildOccasionImage(backgroundImageUrl!), + ), + ); + }, + ); + } +} + +/// Remote-config image URLs may point to either an SVG (e.g. the National +/// Day banner) or a raster image (PNG/JPG). [CachedNetworkImage] cannot +/// decode SVG — it silently renders nothing — so SVG URLs are routed to +/// [SvgPicture.network] instead, based on the URL's file extension. +Widget buildOccasionImage(String url) { + final isSvg = url.split('?').first.toLowerCase().endsWith('.svg'); + if (isSvg) { + return SvgPicture.network( + url, + width: double.infinity, + fit: BoxFit.cover, + placeholderBuilder: (_) => const SizedBox.shrink(), + ); + } + return CachedNetworkImage( + imageUrl: url, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, error) { + log('OccasionImage: failed to load $url -> $error'); + return const SizedBox.shrink(); + }, + ); +} diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index 45023c3b..3c5ebbf7 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -38,7 +38,7 @@ class SmallServiceCard extends StatelessWidget { final Color? backgroundColor; final bool isBold; - SmallServiceCard({ + const SmallServiceCard({ super.key, this.icon = "", this.serviceName = "", diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 5b23bab2..1a5fe039 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -58,7 +58,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { ? AppColors.successColor.withValues(alpha: 0.1) : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) ? AppColors.warningColorYellow.withValues(alpha: 0.1) - : AppColors.primaryRedColorBase.withValues(alpha: 0.1), + : AppColors.primaryRedColor.withValues(alpha: 0.1), textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index 23b14db9..4bc98627 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -37,7 +37,7 @@ class InsuranceApprovalCard extends StatelessWidget { } else if (_isPartiallyApproved()) { return AppColors.warningColorYellow.withValues(alpha: 0.1); } else { - return AppColors.primaryRedColorBase.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); } } @@ -126,7 +126,7 @@ class InsuranceApprovalCard extends StatelessWidget { runSpacing: 4.h, children: [ // AppCustomChipWidget(labelText: isLoading ? "Approved" : insuranceApprovalResponseModel.approvalStatusDescption!, - // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColorBase.withValues(alpha: 0.1), + // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColor.withValues(alpha: 0.1), // textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, // ).toShimmer2(isShow: isLoading), AppCustomChipWidget(labelText: isLoading ? "Cardiology" : insuranceApprovalResponseModel.clinicName ?? "").toShimmer2(isShow: isLoading), diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 2393e875..f2019c91 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -69,8 +69,8 @@ class InsuranceHistory extends StatelessWidget { CustomButton( text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription ?? "", onPressed: () {}, - backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), - borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 10, isBold: true, diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 4b352474..87ae3a97 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -117,7 +117,7 @@ class PatientInsuranceCard extends StatelessWidget { } }, backgroundColor: isCurrentPatientInsuranceExpired(insuranceCardDetailsModel.cardValidTo!) - ? AppColors.primaryRedColorBase.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : insuranceViewModel.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), diff --git a/lib/presentation/medical_file/eye_measurement_details_page.dart b/lib/presentation/medical_file/eye_measurement_details_page.dart index 90dbba10..68f261cd 100644 --- a/lib/presentation/medical_file/eye_measurement_details_page.dart +++ b/lib/presentation/medical_file/eye_measurement_details_page.dart @@ -47,7 +47,7 @@ class EyeMeasurementDetailsPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.classes.tr()), CustomTabBarModel(null, LocaleKeys.contactLens.tr()), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 99a6db10..629ff15c 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -340,7 +340,7 @@ class _MedicalFilePageState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColorBase.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : insuranceVM.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 4b6f820a..76f318f7 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -34,14 +34,13 @@ class _FamilyMedicalScreenState extends State { super.initState(); medicalVM = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) async { - if(medicalVM!.isFamilyFilesLoading) { + if (medicalVM!.isFamilyFilesLoading) { LoaderBottomSheet.showLoader(); } else { LoaderBottomSheet.hideLoader(); } medicalVM?.onFamilyFileTabChange(0); medicalVM?.getAllPendingRecordsByResponseId(); - }); } @@ -71,6 +70,8 @@ class _FamilyMedicalScreenState extends State { medicalVM!.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); }); }, + borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.primaryRedColor, icon: AppAssets.add_icon, fontSize: 16.f, borderRadius: 12.r, @@ -104,9 +105,7 @@ class _FamilyMedicalScreenState extends State { ), shouldRebuild: (previous, next) { // Only rebuild if something actually changed - return previous.selectedIndex != next.selectedIndex || - !identical(previous.patientFiles, next.patientFiles) || - !identical(previous.pendingFiles, next.pendingFiles); + return previous.selectedIndex != next.selectedIndex || !identical(previous.patientFiles, next.patientFiles) || !identical(previous.pendingFiles, next.pendingFiles); }, builder: (context, data, child) => getFamilyTabs( index: data.selectedIndex, @@ -127,7 +126,6 @@ class _FamilyMedicalScreenState extends State { }) { switch (index) { case 0: - return FamilyCards( profiles: patientFiles, onSelect: (FamilyFileResponseModelLists profile) { diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 16130d0c..f6300be3 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -165,67 +165,63 @@ class _FamilyCardsState extends State { title: LocaleKeys.whoCanViewMyMedicalFile.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, initiallyExpanded: true, - children: [ - SizedBox(height: 10.h), - manageFamily() - ], + children: [SizedBox(height: 10.h), manageFamily()], ), ExpandableListItem(title: LocaleKeys.notifications.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, initiallyExpanded: true, children: [ SizedBox(height: 10.h), widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).isEmpty ? Utils.getNoDataWidget(context) : ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).length, - itemBuilder: (context, index) { - final mySideProfiles = widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).toList(); - FamilyFileResponseModelLists profile = mySideProfiles[index]; - return Container( - margin: EdgeInsets.only(bottom: 12.h), - padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), - child: Opacity( - opacity: 1.0, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - AppCustomChipWidget( - labelText: profile.statusDescription ?? " N/A", - backgroundColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.lightGreenColor - : AppColors.lightGrayBGColor, - textColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.textGreenColor - : AppColors.alertColor - ), - SizedBox(height: 8.h), - Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [ - (profile.patientName ?? "").toText14(isBold: true, isCenter: false, maxlines: 1), - (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)) - .toText12(isCenter: false, maxLine: 1, isBold: true, color: AppColors.greyTextColor), - ]), - SizedBox(height: 8.h), - AppCustomChipWidget( - labelText: "Medical File: ${profile.responseId ?? "N/A"}", + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).length, + itemBuilder: (context, index) { + final mySideProfiles = widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).toList(); + FamilyFileResponseModelLists profile = mySideProfiles[index]; + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AppCustomChipWidget( + labelText: profile.statusDescription ?? " N/A", + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor), + SizedBox(height: 8.h), + Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [ + (profile.patientName ?? "").toText14(isBold: true, isCenter: false, maxlines: 1), + (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)) + .toText12(isCenter: false, maxLine: 1, isBold: true, color: AppColors.greyTextColor), + ]), + SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "Medical File: ${profile.responseId ?? "N/A"}", + ), + ], + ), ), - ], - ), + ); + }, ), - ); - }, - ), ]) ], theme: ExpandableListTheme.custom( @@ -410,7 +406,7 @@ class _FamilyCardsState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) + ? AppColors.primaryRedColor.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.warningColorYellow.withValues(alpha: 0.1), @@ -517,7 +513,7 @@ class _FamilyCardsState extends State { }, text: isActive ? LocaleKeys.active.tr(context: context) : LocaleKeys.switchLogin.tr(context: context), backgroundColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, - borderColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: isActive || !canSwitch ? AppColors.greyTextColor : AppColors.primaryRedColor, fontSize: 13.h, icon: isActive ? AppAssets.activeCheck : AppAssets.switch_user, @@ -527,13 +523,15 @@ class _FamilyCardsState extends State { ], ), if (widget.isShowRemoveButton) ...[ - isParentUser ? Positioned( - top: 0, - right: 0, - child: Utils.buildSvgWithAssets(icon: AppAssets.deleteIcon).onPress(() { - if (!isActive) widget.onRemove(profile); - }), - ) : SizedBox(), + isParentUser + ? Positioned( + top: 0, + right: 0, + child: Utils.buildSvgWithAssets(icon: AppAssets.deleteIcon).onPress(() { + if (!isActive) widget.onRemove(profile); + }), + ) + : SizedBox(), ], ], ), @@ -575,7 +573,7 @@ class _FamilyCardsState extends State { backgroundColor: profile.status == FamilyFileEnum.pending.toInt ? AppColors.alertLightColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) + ? AppColors.primaryRedColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.active.toInt ? AppColors.lightGreenColor : AppColors.lightGrayBGColor, diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 5f1b3d3a..8fea72fc 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -201,8 +201,8 @@ class InvoiceListCard extends StatelessWidget { onTap!(); } }, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, @@ -223,7 +223,7 @@ class InvoiceListCard extends StatelessWidget { // icon: AppAssets.refundIcon, // iconColor: AppColors.caloriesCalculatorColor, // backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), - // borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), + // borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), // textColor: AppColors.caloriesCalculatorColor, // fontSize: 14.f, // fontWeight: FontWeight.w600, diff --git a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart index 4c8be01e..2aaefcac 100644 --- a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart +++ b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart @@ -57,7 +57,7 @@ class PharmacyInvoiceCard extends StatelessWidget { // if (pharmacyInvoice.totalAmount != null) // AppCustomChipWidget( // labelText: '${pharmacyInvoice.totalAmount!.toStringAsFixed(2)} ${LocaleKeys.sar.tr(context: context)}', - // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), // textColor: AppColors.primaryRedColor, // isEnglishOnly: true, // ), @@ -158,8 +158,8 @@ class PharmacyInvoiceCard extends StatelessWidget { ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context), onPressed: onTap, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index af6623c8..ce300c9b 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -52,14 +52,13 @@ class ProfileSettings extends StatefulWidget { } class ProfileSettingsState extends State { - final InAppReview inAppReview = InAppReview.instance; @override void initState() { super.initState(); _loadPermissions(); - + // Load data after first frame WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -434,7 +433,15 @@ class ProfileSettingsState extends State { if (trailingLabel.isNotEmpty) ConstrainedBox( constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.35), - child: isEnglishOnly ? Directionality(textDirection: ui.TextDirection.ltr, child: trailingLabel.toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly).toShimmer2(isShow: isShowLoading)) : trailingLabel.toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly).toShimmer2(isShow: isShowLoading), + child: isEnglishOnly + ? Directionality( + textDirection: ui.TextDirection.ltr, + child: trailingLabel + .toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly) + .toShimmer2(isShow: isShowLoading)) + : trailingLabel + .toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly) + .toShimmer2(isShow: isShowLoading), ), switchValue != null ? Switch( @@ -452,8 +459,10 @@ class ProfileSettingsState extends State { ); } - Widget socialMedia(String icon, String label, - ) { + Widget socialMedia( + String icon, + String label, + ) { return SizedBox( height: 56.h, child: Row( @@ -461,14 +470,13 @@ class ProfileSettingsState extends State { children: [ Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.greyTextColor), label.toText14(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, - Row( spacing: 24.w, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.x2).onPress(()=> launchUrl(Uri.parse("https://twitter.com/HMG"))), - Utils.buildSvgWithAssets(icon: AppAssets.fb).onPress(()=> launchUrl(Uri.parse("https://www.facebook.com/DrSulaimanAlHabib?ref=tn_tnmn"))), - Utils.buildSvgWithAssets(icon: AppAssets.linkedin).onPress(()=> launchUrl(Uri.parse("https://www.linkedin.com/company/drsulaiman-alhabib-medical-group"))), - Utils.buildSvgWithAssets(icon: AppAssets.youtube).onPress(()=> launchUrl(Uri.parse("https://www.youtube.com/c/DrsulaimanAlhabibHospitals"))), + Utils.buildSvgWithAssets(icon: AppAssets.x2).onPress(() => launchUrl(Uri.parse("https://twitter.com/HMG"))), + Utils.buildSvgWithAssets(icon: AppAssets.fb).onPress(() => launchUrl(Uri.parse("https://www.facebook.com/DrSulaimanAlHabib?ref=tn_tnmn"))), + Utils.buildSvgWithAssets(icon: AppAssets.linkedin).onPress(() => launchUrl(Uri.parse("https://www.linkedin.com/company/drsulaiman-alhabib-medical-group"))), + Utils.buildSvgWithAssets(icon: AppAssets.youtube).onPress(() => launchUrl(Uri.parse("https://www.youtube.com/c/DrsulaimanAlhabibHospitals"))), ], ) ], @@ -546,9 +554,7 @@ class _FamilyCardWidgetState extends State { // final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; // final canSwitch = isParentUser || (!isParentUser && widget.profile.responseId == appState.getSuperUserID); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, hasShadow: true, hasDenseShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, hasDenseShadow: true), child: Column( children: [ Column( @@ -607,12 +613,14 @@ class _FamilyCardWidgetState extends State { labelText: "${LocaleKeys.fileno.tr(context: context)}: ${widget.profile.responseId}", iconSize: 12.w, ), - isActive ? AppCustomChipWidget( - icon: AppAssets.checkmark_icon, - labelText: LocaleKeys.verified.tr(context: context), - iconColor: AppColors.successColor, - labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - ) : SizedBox.shrink(), + isActive + ? AppCustomChipWidget( + icon: AppAssets.checkmark_icon, + labelText: LocaleKeys.verified.tr(context: context), + iconColor: AppColors.successColor, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), + ) + : SizedBox.shrink(), ], ), ], @@ -639,73 +647,76 @@ class _FamilyCardWidgetState extends State { ), // isActive && appState.getAuthenticatedUser()!.bloodGroup != null // ? - isActive ? AppCustomChipWidget( - icon: AppAssets.blood_icon, + isActive + ? AppCustomChipWidget( + icon: AppAssets.blood_icon, labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), - labelText: appState.getAuthenticatedUser()!.bloodGroup ?? "N/A", - iconColor: AppColors.primaryRedColor) - : SizedBox(), + labelText: appState.getAuthenticatedUser()!.bloodGroup ?? "N/A", + iconColor: AppColors.primaryRedColor) + : SizedBox(), Consumer(builder: (context, insuranceVM, child) { return isActive ? AppCustomChipWidget( icon: insuranceVM.isInsuranceExpired ? AppAssets.cancel_circle_icon : insuranceVM.isInsuranceActive - ? AppAssets.insurance_active_icon - : AppAssets.alertSquare, - labelText: insuranceVM.isInsuranceExpired - ? LocaleKeys.insuranceExpired.tr(context: context) - : insuranceVM.isInsuranceActive - ? LocaleKeys.insuranceActive.tr(context: context) - : LocaleKeys.insuranceInActive.tr(context: context), - iconColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - textColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - iconSize: 12.w, - deleteIcon: insuranceVM.isInsuranceActive ? null : AppAssets.forward_chevron_icon, - deleteIconColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - deleteIconHasColor: true, - onChipTap: () { - if (!insuranceVM.isInsuranceActive) { - insuranceVM.setIsInsuranceUpdateDetailsLoading(true); - insuranceVM.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); - // showCommonBottomSheetWithoutHeight( - // title: LocaleKeys.notice.tr(context: context), - // context, - // child: Utils.getWarningWidget( - // loadingText: LocaleKeys.insuranceInActiveContactSupport.tr(context: context), - // confirmText: LocaleKeys.contactUs.tr(context: context), - // isShowActionButtons: true, - // onCancelTap: () { - // Navigator.pop(context); - // }, - // onConfirmTap: () async { - // launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); - // }), - // callBackFunc: () {}, - // isFullScreen: false, - // isCloseButtonVisible: true, - // ); - } - }, - backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) - : insuranceVM.isInsuranceActive - ? AppColors.successColor.withValues(alpha: 0.1) - : AppColors.warningColorYellow.withValues(alpha: 0.1), - labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), + ? AppAssets.insurance_active_icon + : AppAssets.alertSquare, + labelText: insuranceVM.isInsuranceExpired + ? LocaleKeys.insuranceExpired.tr(context: context) + : insuranceVM.isInsuranceActive + ? LocaleKeys.insuranceActive.tr(context: context) + : LocaleKeys.insuranceInActive.tr(context: context), + iconColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + textColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + iconSize: 12.w, + deleteIcon: insuranceVM.isInsuranceActive ? null : AppAssets.forward_chevron_icon, + deleteIconColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + deleteIconHasColor: true, + onChipTap: () { + if (!insuranceVM.isInsuranceActive) { + insuranceVM.setIsInsuranceUpdateDetailsLoading(true); + insuranceVM.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + // showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.notice.tr(context: context), + // context, + // child: Utils.getWarningWidget( + // loadingText: LocaleKeys.insuranceInActiveContactSupport.tr(context: context), + // confirmText: LocaleKeys.contactUs.tr(context: context), + // isShowActionButtons: true, + // onCancelTap: () { + // Navigator.pop(context); + // }, + // onConfirmTap: () async { + // launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); + // }), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + } + }, + backgroundColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor.withValues(alpha: 0.1) + : insuranceVM.isInsuranceActive + ? AppColors.successColor.withValues(alpha: 0.1) + : AppColors.warningColorYellow.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), ).toShimmer2(isShow: insuranceVM.isInsuranceLoading) : SizedBox.shrink(); }), @@ -747,7 +758,7 @@ class _FamilyCardWidgetState extends State { height: isFoldable ? 50.h : 45.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); - } + } Widget _buildNonParentUserButton(int? superUserId) { final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId; diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index 92729d8a..fbd1d58b 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -123,7 +123,7 @@ class FamilyCardWidget extends StatelessWidget { icon = AppAssets.cancel_circle_icon; labelText = LocaleKeys.insuranceExpired.tr(context: context); iconColor = AppColors.primaryRedColor; - backgroundColor = AppColors.primaryRedColorBase.withValues(alpha: 0.15); + backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); } else { icon = AppAssets.insurance_active_icon; labelText = LocaleKeys.insuranceActive.tr(context: context); diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index d68b7494..630c5dc0 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -173,8 +173,8 @@ class _RefundHistoryScreenState extends State { CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))), - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 438d9728..5b5d8f2e 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -480,7 +480,7 @@ class _OrganSelectorPageState extends State with SingleTicker backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, deleteIcon: AppAssets.cancel, - deleteIconColor: AppColors.primaryRedColor, + deleteIconColor: AppColors.criticalActionColor, deleteIconHasColor: false, onDeleteTap: () { viewModel.removeOrgan(organ.id); @@ -500,6 +500,7 @@ class _OrganSelectorPageState extends State with SingleTicker isDisabled: viewModel.selectedOrgans.isEmpty, backgroundColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, + borderColor: Colors.transparent, ), ); } diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index c9f07c96..0e150733 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -289,7 +289,7 @@ class _RiskFactorsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index e49f0521..5e102581 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -268,7 +268,7 @@ class _SuggestionsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 2e57664a..6a83316b 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -360,7 +360,7 @@ class _SymptomsSelectorPageState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 24a8ddcd..95de9464 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -750,7 +750,7 @@ class _TriagePageState extends State { percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.17), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( @@ -785,7 +785,7 @@ class _TriagePageState extends State { text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, isDisabled: viewModel.isTriageDiagnosisLoading, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart index 3f9c5747..6672a968 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -148,7 +148,7 @@ class _UserInfoFlowManagerState extends State { boxShadow: isCurrentStep && value > 0 ? [ BoxShadow( - color: AppColors.primaryRedColorBase.withValues(alpha: 0.4), + color: AppColors.primaryRedColor.withValues(alpha: 0.4), blurRadius: 8.r, spreadRadius: 1.r, ), @@ -191,8 +191,8 @@ class _UserInfoFlowManagerState extends State { isDisabled: !(viewModel.isCurrentUserInfoPageValid), onPressed: () => viewModel.isCurrentUserInfoPageValid ? _onNext() : () {}, backgroundColor: - viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), - borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), + viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), + borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), textColor: AppColors.whiteColor, fontSize: 16.f, ) @@ -204,7 +204,7 @@ class _UserInfoFlowManagerState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPrevious, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection_screen.dart b/lib/presentation/symptoms_checker/user_info_selection_screen.dart index fef576dc..589771ec 100644 --- a/lib/presentation/symptoms_checker/user_info_selection_screen.dart +++ b/lib/presentation/symptoms_checker/user_info_selection_screen.dart @@ -296,7 +296,7 @@ class _UserInfoSelectionScreenState extends State { context.read().setUserInfoPage(0, isSinglePageEdit: false); context.navigateWithName(AppRoutes.userInfoFlowManager); }, - backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart index 073aa110..307445a4 100644 --- a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart +++ b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart @@ -188,7 +188,7 @@ class _PinchZoomTutorialOverlayState extends State wit width: 48.w, height: 48.h, decoration: BoxDecoration( - color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + color: AppColors.primaryRedColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), ), child: Icon( diff --git a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart index e2b342b6..a4f5f1ee 100644 --- a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart +++ b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart @@ -121,7 +121,7 @@ class _SelectedOrgansSectionState extends State { backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, deleteIcon: AppAssets.cross_circle, - deleteIconColor: AppColors.primaryRedColor, + deleteIconColor: AppColors.criticalActionColor, deleteIconHasColor: true, onDeleteTap: () => widget.onRemoveOrgan(organ.id), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), diff --git a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart index b1e8beea..8633d830 100644 --- a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart +++ b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart @@ -94,7 +94,7 @@ class _TriageLevelCardState extends State with SingleTickerProv switch (triageLevel) { case 'emergency_ambulance': case 'emergency': - return AppColors.primaryRedColorBase.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); case 'consultation_24': case 'consultation': return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1); diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart index 2d61378b..aef892f9 100644 --- a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -91,7 +91,7 @@ class SwitchCupBottomSheet extends StatelessWidget { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isSelected ? AppColors.primaryRedColorBase.withOpacity(0.08) : Colors.transparent, + color: isSelected ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, @@ -271,9 +271,9 @@ class _CustomizeCupBottomSheetState extends State { SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.primaryRedColor, - inactiveTrackColor: AppColors.primaryRedColorBase.withOpacity(0.2), + inactiveTrackColor: AppColors.secondaryLightRedColor, thumbColor: AppColors.primaryRedColor, - overlayColor: AppColors.primaryRedColorBase.withOpacity(0.2), + overlayColor: AppColors.secondaryLightRedColor, trackHeight: 4.h, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), ), diff --git a/lib/presentation/weather/weather_details_page.dart b/lib/presentation/weather/weather_details_page.dart index 7e93b9b6..50dbefa3 100644 --- a/lib/presentation/weather/weather_details_page.dart +++ b/lib/presentation/weather/weather_details_page.dart @@ -62,7 +62,7 @@ class _WeatherDetailsPageState extends State { case ColorName.ORANGE: return AppColors.warningLightColor.withValues(alpha: 0.1); case ColorName.RED: - return AppColors.primaryRedColorBase.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); } } @@ -120,7 +120,7 @@ class _WeatherDetailsPageState extends State { } // Red: Very High, At High Risk else if (category.contains('very high') || category.contains('at high risk')) { - return AppColors.primaryRedColorBase.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); } // Orange: Risk, High (but not "very high" or "at high risk") else if (category.contains('risk') || category.contains('high')) { diff --git a/lib/services/remote_config_service.dart b/lib/services/remote_config_service.dart index e8a7ec16..568bd4e6 100644 --- a/lib/services/remote_config_service.dart +++ b/lib/services/remote_config_service.dart @@ -81,4 +81,3 @@ class RemoteConfigServiceImp implements RemoteConfigService { static const String _fallbackDisabledJson = '{"key":"none","enabled":false}'; } - diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 2be811d8..24c8bd25 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -14,20 +14,33 @@ class AppColors { /// (e.g. National Day). Null unless an occasion theme is active. static Color? occasionPrimaryColor; + /// Lighter/translucent (11% opacity) variant of [occasionPrimaryColor], + /// derived automatically by OccasionThemeViewModel. Null unless an + /// occasion theme is active. + static Color? occasionSecondaryColor; + static const transparent = Colors.transparent; // ── Scaffold / Background ───────────────────────────────────────────────── static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); + static Color get bottomSheetBgColor => isDarkMode ? dark.bottomSheetBgColor : const Color(0xFFF8F8FA); + static Color get lightGreyEFColor => isDarkMode ? dark.lightGreyEFColor : const Color(0xffeaeaff); + static Color get greyF7Color => isDarkMode ? dark.greyF7Color : const Color(0xffF7F7F7); + static Color get greyInfoTextColor => isDarkMode ? dark.greyInfoTextColor : const Color(0xff777777); + static Color get lightGrayColor => isDarkMode ? dark.lightGrayColor : const Color(0xff808080); + static Color get greyTextColorLight => isDarkMode ? dark.greyTextColorLight : const Color(0xFFA2A2A2); + static Color get mainPurple => isDarkMode ? dark.mainPurple : const Color(0xFF7954F7); // ── Core Colors ─────────────────────────────────────────────────────────── static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFffffff); + // static const Color primaryRedColor = Color(0xFFED1C2B); /// Occasion-aware primary brand color. Resolves to the remotely-configured /// occasion color when active (e.g. National Day green), otherwise falls @@ -36,32 +49,71 @@ class AppColors { /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived /// (translucent) colors — use [primaryRedColorBase] for those instead, so /// occasion theming never affects translucent overlays/backgrounds. - static Color get primaryRedColor => - (isOccasionThemeActive && occasionPrimaryColor != null) ? occasionPrimaryColor! : primaryRedColorBase; + static Color get primaryRedColor => (isOccasionThemeActive && occasionPrimaryColor != null) ? occasionPrimaryColor! : primaryRedColorBase; /// The original, non-themed brand red (or its dark-mode variant). Always /// stable regardless of any active occasion theme — intended for opacity /// derived usages (e.g. `AppColors.primaryRedColorBase.withOpacity(0.1)`). - static Color get primaryRedColorBase => isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B); - static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); + static Color get primaryRedColorBase => isDarkMode ? const Color(0xFFED1C2B) : const Color(0xFFED1C2B); + + /// Semantic color for critical/negative actions (delete/deactivate account, + /// cancel appointment, clear/reset, emergency, etc.). Same light/dark + /// values as [primaryRedColorBase] — respects dark mode, but is never + /// affected by the active occasion/remote-config theme, so destructive + /// actions always read as "red" regardless of any occasion branding. + static Color get criticalActionColor => primaryRedColorBase; + + /// Occasion-aware light/tint variant of [primaryRedColor] (e.g. chip and + /// badge backgrounds). Resolves to the remotely-configured occasion's + /// secondary color when active (e.g. National Day's light green), + /// otherwise falls back to the original dark/light tint — identical to + /// current behavior. + /// + /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived + /// colors — use [secondaryLightRedColorBase] for those instead, so + /// occasion theming never affects translucent overlays/backgrounds. + static Color get secondaryLightRedColor => (isOccasionThemeActive && occasionSecondaryColor != null) ? occasionSecondaryColor! : secondaryLightRedColorBase; + + /// The original, non-themed light-red tint (or its dark-mode variant). + /// Always stable regardless of any active occasion theme — intended for + /// opacity derived usages. + static Color get secondaryLightRedColorBase => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); static const Color successColor = Color(0xFF18C273); + static Color get successLightBgColor => isDarkMode ? dark.successLightBgColor : const Color(0xffDDF6EA); + static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); + static Color get chipBgColor => isDarkMode ? dark.chipBgColor : const Color(0xFFEFEFF0); + static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); + static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); + static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); + static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); + static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); + static Color get warningColorYellow => isDarkMode ? dark.warningColorYellow : const Color(0xFFF4A308); + static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xff898A8D); + static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); + static Color get lightGrayBGColor => isDarkMode ? dark.lightGrayBGColor : const Color(0x142E3039); + static Color get checkBoxBorderColor => isDarkMode ? dark.checkBoxBorderColor : const Color(0xffD2D2D2); + static Color get pharmacyBGColor => isDarkMode ? dark.pharmacyBGColor : const Color(0xFF359846); + static Color get lightGreenColor => isDarkMode ? dark.lightGreenColor : const Color(0xFF0ccedde); + static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); + static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); + static Color get habibPharmacyColor => const Color(0xFF2d9137); // ── Doctor Rating Colors ────────────────────────────────────────────────── @@ -74,92 +126,147 @@ class AppColors { static const Color ratingBadgeFiveStarColor = Color(0xFF5BBD8E); // Green badge for ≥4.0 static const Color ratingPercentageTextColor = Color(0xFF4A4A4A); // Dark grey for percentage text - // ── Semantic / Status Colors ────────────────────────────────────────────── static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); + static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05); + static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); + static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); + static Color get switchBackgroundColor => isDarkMode ? dark.switchBackgroundColor : const Color(0x2618C273); + static Color get bottomNAVBorder => isDarkMode ? dark.bottomNAVBorder : const Color(0xFFEEEEEE); + static Color get quickLoginColor => isDarkMode ? dark.quickLoginColor : const Color(0xFF666666); + static Color get tooltipTextColor => isDarkMode ? dark.tooltipTextColor : const Color(0xFF414D55); + static Color get graphGridColor => isDarkMode ? dark.graphGridColor : const Color(0x4D18C273); + static Color get highAndLow => isDarkMode ? dark.highAndLow : const Color(0xFFFFAF15); + static Color get labelTextColor => isDarkMode ? dark.labelTextColor : const Color(0xFF838383); + static Color get calenderTextColor => isDarkMode ? dark.calenderTextColor : const Color(0xFFD0D0D0); + static Color get lightGreenButtonColor => isDarkMode ? dark.lightGreenButtonColor : const Color(0x2618C273); + static Color get lightRedButtonColor => isDarkMode ? dark.lightRedButtonColor : const Color(0x1AED1C2B); // ── Status Colors ───────────────────────────────────────────────────────── static Color get statusPendingColor => isDarkMode ? dark.statusPendingColor : const Color(0xffCC9B14); + static Color get statusProcessingColor => isDarkMode ? dark.statusProcessingColor : const Color(0xff2E303A); + static Color get statusCompletedColor => isDarkMode ? dark.statusCompletedColor : const Color(0xff359846); + static Color get statusRejectedColor => isDarkMode ? dark.statusRejectedColor : const Color(0xffD02127); // ── Info Banner Colors ──────────────────────────────────────────────────── static Color get infoBannerBgColor => isDarkMode ? dark.infoBannerBgColor : const Color(0xFFFFF4E6); + static Color get infoBannerBorderColor => isDarkMode ? dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + static Color get infoBannerIconColor => isDarkMode ? dark.infoBannerIconColor : const Color(0xFFCC9B14); + static Color get infoBannerTextColor => isDarkMode ? dark.infoBannerTextColor : const Color(0xFF856404); // ── Symptoms Checker ─────────────────────────────────────────────��──────── static Color get chipColorSeekMedicalAdvice => isDarkMode ? dark.chipColorSeekMedicalAdvice : const Color(0xFFFFAF15); + static Color get chipTextColorSeekMedicalAdvice => isDarkMode ? dark.chipTextColorSeekMedicalAdvice : const Color(0xFFD48D05); // ── Services Page Colors ────────────────────────────────────────────────── static Color get eReferralCardColor => isDarkMode ? dark.eReferralCardColor : const Color(0xFFFF8012); + static Color get bloodDonationCardColor => isDarkMode ? dark.bloodDonationCardColor : const Color(0xFFFF5662); + static Color get myChildVaccineCardColor => isDarkMode ? dark.myChildVaccineCardColor : const Color(0xFFFF2D78); + static Color get bookAppointment => isDarkMode ? dark.bookAppointment : const Color(0xFF415364); // ── Health Calculators Colors ───────────────────────────────────────────── static Color get bmiCalculatorColor => isDarkMode ? dark.bmiCalculatorColor : const Color(0xFFF43333); + static Color get caloriesCalculatorColor => isDarkMode ? dark.caloriesCalculatorColor : const Color(0xFF0B85F7); + static Color get bmrCalculatorColor => isDarkMode ? dark.bmrCalculatorColor : const Color(0xFF18C273); + static Color get idealBodyWeightColor => isDarkMode ? dark.idealBodyWeightColor : const Color(0xFFFF8012); + static Color get bodyFatCalculatorColor => isDarkMode ? dark.bodyFatCalculatorColor : const Color(0xFFFF5662); + static Color get carbsProteinFatColor => isDarkMode ? dark.carbsProteinFatColor : const Color(0xFF18C273); + static Color get ovulationPeriodColor => isDarkMode ? dark.ovulationPeriodColor : const Color(0xFFFF47A9); + static Color get deliveryDueDateColor => isDarkMode ? dark.deliveryDueDateColor : const Color(0xFFFF2D78); // ── Water Monitor ───────────────────────────────────────────────────────── static Color get blueColor => isDarkMode ? dark.blueColor : const Color(0xFF4EB5FF); + static Color get blueGradientColorOne => isDarkMode ? dark.blueGradientColorOne : const Color(0xFFF1F7FD); + static Color get blueGradientColorTwo => isDarkMode ? dark.blueGradientColorTwo : const Color(0xFFD9EFFF); // ── Shimmer ─────────────────────────────────────────────────────────────── static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); + static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); + static Color get covid29Color => isDarkMode ? dark.covid29Color : const Color(0xff2563EB); + static Color get lightGreyTextColor => isDarkMode ? dark.lightGreyTextColor : const Color(0xFF959595); + static Color get labelColorYellow => isDarkMode ? dark.labelColorYellow : const Color(0xFFFBCB6E); - static const LinearGradient aiLinearGradient = - LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); + static const LinearGradient aiLinearGradient = LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); // ── Aliases ─────────────────────────────────────────────────────────────── static Color get bgScaffoldColor => scaffoldBgColor; + static Color get primaryRedBorderColor => primaryRedColor; + static Color get chipPrimaryRedBorderColor => primaryRedColor; + static Color get criticalLowAndHigh => primaryRedColor; + static Color get chipColorEmergency => primaryRedColor; + static Color get errorLightColor => primaryRedColor; + static Color get secondaryLightRedBorderColor => secondaryLightRedColor; + static Color get bgRedLightColor => secondaryLightRedColor; + static Color get chipSecondaryLightRedColor => secondaryLightRedColor; + static Color get blackBgColor => textColor; + static Color get blackColor => textColor; + static Color get borderOnlyColor => textColor; + static Color get chipBorderColorOpacity20 => borderGrayColor; + static Color get bgGreenColor => successColor; + static Color get textGreenColor => successColor; + static Color get successLightColor => successColor; + static Color get thumbColor => successColor; + static Color get chipColorMonitor => successColor; + static Color get greyLightColor => greyColor; + static Color get alertLightColor => alertColor; + static Color get infoLightColor => infoColor; + static Color get warningLightColor => warningColor; // ── Dark Mode Palette ───────────────────────────────────────────────────── @@ -174,11 +281,17 @@ class AppColorsDark { // Scaffold / Background Color get scaffoldBgColor => const Color(0xFF191919); + Color get bottomSheetBgColor => const Color(0xFF1C1C1E); + Color get lightGreyEFColor => const Color(0xFF2A2A3A); + Color get greyF7Color => const Color(0xFF1E1E1E); + Color get greyInfoTextColor => const Color(0xFF9E9E9E); + Color get lightGrayColor => const Color(0xFF9E9E9E); + Color get greyTextColorLight => const Color(0xFF757575); // ⚠️ NEW — was missing from AppColorsDark @@ -186,114 +299,192 @@ class AppColorsDark { // Core Color get baseColor => const Color(0xFF1E1E1E); + // ⚠️ NEW — maps whiteColor → dark surface color Color get whiteColor => const Color(0xFF1E1E1E); // surface replacement for white Color get primaryRedColor => const Color(0xFFDE5C5D); + Color get secondaryLightRedColor => const Color(0xFF3A1015); + Color get successColor => const Color(0xFF18C273); + Color get successLightBgColor => const Color(0xFF0D2E1F); + Color get textColor => const Color(0xFFFFFFFF); + Color get iconColor => const Color(0xFFFFFFFF); + Color get borderGrayColor => const Color(0x55ECECEC); + Color get textColorLight => const Color(0xFFB0B0B0); + Color get dividerColor => const Color(0x33FFFFFF); + Color get warningColorYellow => const Color(0xFFF4A308); + Color get inputLabelTextColor => const Color(0xFF9E9E9E); + Color get greyTextColor => const Color(0xFF8F9AA3); + Color get lightGrayBGColor => const Color(0x22FFFFFF); + Color get checkBoxBorderColor => const Color(0xFF555555); + Color get pharmacyBGColor => const Color(0xFF359846); + // ⚠️ NEW — was missing from AppColorsDark Color get lightGreenColor => const Color(0xFF0ccedde); // same teal accent — works on dark Color get ratingColorYellow => const Color(0xFFFFAF15); + Color get spacerLineColor => const Color(0x22FFFFFF); // Semantic / Status Color get errorColor => const Color(0xFFD63D48); + Color get alertColor => const Color(0xFFD48D05); + Color get infoColor => const Color(0xFF4DA6FF); + Color get warningColor => const Color(0xFFFFCC00); + Color get greyColor => const Color(0xFF2C2C2E); + Color get chipBgColor => const Color(0xFF3E3E3E); + Color get cardDarkBorderColor => const Color(0x0DEAEAEA); + Color get serviceCardButtonColor => const Color(0xFF3E3E3E); + Color get switchBackgroundColor => const Color(0x2618C273); + Color get bottomNAVBorder => const Color(0xFF2C2C2E); + Color get quickLoginColor => const Color(0xFF9E9E9E); + Color get tooltipTextColor => const Color(0xFFCFD8DC); + Color get graphGridColor => const Color(0x4D18C273); + Color get highAndLow => const Color(0xFFFFAF15); + Color get labelTextColor => const Color(0xFF9E9E9E); + Color get calenderTextColor => const Color(0xFF555555); + Color get lightGreenButtonColor => const Color(0x2618C273); + Color get lightRedButtonColor => const Color(0x1AED1C2B); // Status Color get statusPendingColor => const Color(0xFFCC9B14); + Color get statusProcessingColor => const Color(0xFFECECEC); + Color get statusCompletedColor => const Color(0xFF359846); + Color get statusRejectedColor => const Color(0xFFD02127); // Info Banner Color get infoBannerBgColor => const Color(0xFF2A1F0A); + Color get infoBannerBorderColor => const Color(0xFF5A3D0A); + Color get infoBannerIconColor => const Color(0xFFCC9B14); + Color get infoBannerTextColor => const Color(0xFFFFCB6B); // Symptoms Checker Color get chipColorSeekMedicalAdvice => const Color(0xFFFFAF15); + Color get chipTextColorSeekMedicalAdvice => const Color(0xFFD48D05); // Services Page Color get eReferralCardColor => const Color(0xFFFF8012); + Color get bloodDonationCardColor => const Color(0xFFFF5662); + Color get myChildVaccineCardColor => const Color(0xFFFF2D78); + Color get bookAppointment => const Color(0xFF415364); // Health Calculators Color get bmiCalculatorColor => const Color(0xFFF43333); + Color get caloriesCalculatorColor => const Color(0xFF0B85F7); + Color get bmrCalculatorColor => const Color(0xFF18C273); + Color get idealBodyWeightColor => const Color(0xFFFF8012); + Color get bodyFatCalculatorColor => const Color(0xFFFF5662); + Color get carbsProteinFatColor => const Color(0xFF18C273); + Color get ovulationPeriodColor => const Color(0xFFFF47A9); + Color get deliveryDueDateColor => const Color(0xFFFF2D78); // Water Monitor Color get blueColor => const Color(0xFF4EB5FF); + Color get blueGradientColorOne => const Color(0xFF101C2A); + Color get blueGradientColorTwo => const Color(0xFF0D2235); // Shimmer Color get shimmerBaseColor => const Color(0xFF2C2C2C); + Color get shimmerHighlightColor => const Color(0xFF3D3D3D); + Color get covid29Color => const Color(0xFF2563EB); + Color get lightGreyTextColor => const Color(0xFF757575); + Color get labelColorYellow => const Color(0xFFFBCB6E); // Aliases Color get bgScaffoldColor => scaffoldBgColor; + Color get primaryRedBorderColor => primaryRedColor; + Color get chipPrimaryRedBorderColor => primaryRedColor; + Color get criticalLowAndHigh => primaryRedColor; + Color get chipColorEmergency => primaryRedColor; + Color get errorLightColor => primaryRedColor; + Color get secondaryLightRedBorderColor => secondaryLightRedColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get chipSecondaryLightRedColor => secondaryLightRedColor; + Color get blackBgColor => textColor; + Color get blackColor => textColor; + Color get borderOnlyColor => textColor; + Color get chipBorderColorOpacity20 => borderGrayColor; + Color get bgGreenColor => successColor; + Color get textGreenColor => successColor; + Color get successLightColor => successColor; + Color get thumbColor => successColor; + Color get chipColorMonitor => successColor; + Color get greyLightColor => greyColor; + Color get alertLightColor => alertColor; + Color get infoLightColor => infoColor; + Color get warningLightColor => warningColor; } @@ -305,61 +496,99 @@ extension AppColorsContext on BuildContext { // Scaffold / Background Color get scaffoldBgColor => _isDark ? AppColors.dark.scaffoldBgColor : const Color(0xFFF8F8F8); + Color get bottomSheetBgColor => _isDark ? AppColors.dark.bottomSheetBgColor : const Color(0xFFF8F8FA); + Color get greyF7Color => _isDark ? AppColors.dark.greyF7Color : const Color(0xffF7F7F7); + Color get greyInfoTextColor => _isDark ? AppColors.dark.greyInfoTextColor : const Color(0xff777777); + Color get lightGrayColor => _isDark ? AppColors.dark.lightGrayColor : const Color(0xff808080); // Core Color get cardBaseColor => _isDark ? AppColors.dark.baseColor : const Color(0xFFFFFFFF); + Color get primaryRedColor => _isDark ? AppColors.dark.primaryRedColor : AppColors.primaryRedColor; + Color get secondaryLightRedColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFFFEE9EA); + Color get secondaryLightRedForegroundColor => _isDark ? const Color(0xFFFFFFFF) : AppColors.primaryRedColor; + Color get successColor => AppColors.successColor; + Color get successLightBgColor => _isDark ? AppColors.dark.successLightBgColor : const Color(0xffDDF6EA); + Color get textColor => _isDark ? AppColors.dark.textColor : const Color(0xFF2E3039); + Color get iconColor => _isDark ? AppColors.dark.iconColor : const Color(0xFF2E3039); + Color get borderGrayColor => _isDark ? AppColors.dark.borderGrayColor : const Color(0x332E3039); + Color get textColorLight => _isDark ? AppColors.dark.textColorLight : const Color(0xFF5E5E5E); + Color get dividerColor => _isDark ? AppColors.dark.dividerColor : const Color(0x40D2D2D2); + Color get inputLabelTextColor => _isDark ? AppColors.dark.inputLabelTextColor : const Color(0xff898A8D); + Color get greyTextColor => AppColors.greyTextColor; + Color get lightGrayBGColor => _isDark ? AppColors.dark.lightGrayBGColor : const Color(0x142E3039); + Color get checkBoxBorderColor => _isDark ? AppColors.dark.checkBoxBorderColor : const Color(0xffD2D2D2); // Status / Semantic Color get errorColor => _isDark ? AppColors.dark.errorColor : const Color(0xFFED1C2B); + Color get alertColor => AppColors.alertColor; + Color get infoColor => _isDark ? AppColors.dark.infoColor : const Color(0xFF0B85F7); + Color get warningColor => AppColors.warningColor; + Color get greyColor => _isDark ? AppColors.dark.greyColor : const Color(0xFFEFEFF0); + Color get chipBgColor => _isDark ? AppColors.dark.chipBgColor : const Color(0xFFEFEFF0); + Color get cardDarkBorderColor => _isDark ? AppColors.dark.cardDarkBorderColor : Colors.transparent; + Color get serviceCardButtonColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFF2E3039); + Color get bottomNAVBorder => _isDark ? AppColors.dark.bottomNAVBorder : const Color(0xFFEEEEEE); + Color get labelTextColor => _isDark ? AppColors.dark.labelTextColor : const Color(0xFF838383); + Color get lightGreyTextColor => _isDark ? AppColors.dark.lightGreyTextColor : const Color(0xFF959595); // Info Banner Color get infoBannerBgColor => _isDark ? AppColors.dark.infoBannerBgColor : const Color(0xFFFFF4E6); + Color get infoBannerBorderColor => _isDark ? AppColors.dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + Color get infoBannerIconColor => AppColors.infoBannerIconColor; + Color get infoBannerTextColor => _isDark ? AppColors.dark.infoBannerTextColor : const Color(0xFF856404); // Water Monitor Color get blueColor => AppColors.blueColor; + Color get blueGradientColorOne => _isDark ? AppColors.dark.blueGradientColorOne : const Color(0xFFF1F7FD); + Color get blueGradientColorTwo => _isDark ? AppColors.dark.blueGradientColorTwo : const Color(0xFFD9EFFF); // Shimmer Color get shimmerBaseColor => _isDark ? AppColors.dark.shimmerBaseColor : const Color(0xFFE0E0E0); + Color get shimmerHighlightColor => _isDark ? AppColors.dark.shimmerHighlightColor : const Color(0xFFF5F5F5); static const Color tooltipColor = Color(0xFF1AACACAC); // Aliases Color get bgScaffoldColor => scaffoldBgColor; + Color get bgGreenColor => successColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get blackColor => textColor; + Color get blackBgColor => textColor; } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index c3ebe4ba..0c677002 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -115,8 +115,7 @@ class CollapsingListView extends StatelessWidget { margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Transform.flip( flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), + child: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), ), ).onPress(() { if (leadingCallback != null) { @@ -289,8 +288,7 @@ class _ScrollAnimatedTitleState extends State { return Container( // height: (widget.preferredSize.height - _fontSize / 2).h, height: 56.h, - alignment: - isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), + alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Row( spacing: 4.h, @@ -306,38 +304,19 @@ class _ScrollAnimatedTitleState extends State { ), ).expanded, ...[ - if (widget.logout != null) - actionButton(context, t, title: LocaleKeys.logout.tr(context: context), icon: AppAssets.logout).onPress(widget.logout!), - if (widget.report != null) - actionButton(context, t, title: LocaleKeys.feedback.tr(context: context), icon: AppAssets.report_icon).onPress(widget.report!), - if (widget.history != null) - actionButton(context, t, title: LocaleKeys.history.tr(context: context), icon: AppAssets.insurance_history_icon) - .onPress(widget.history!), - if (widget.instructions != null) - actionButton(context, t, title: LocaleKeys.instructions.tr(context: context), icon: AppAssets.requests).onPress(widget.instructions!), - if (widget.requests != null) - actionButton(context, t, title: LocaleKeys.requests.tr(context: context), icon: AppAssets.insurance_history_icon) - .onPress(widget.requests!), - if (widget.sendEmail != null) - actionButton(context, t, title: LocaleKeys.sendEmail.tr(context: context), icon: AppAssets.email).onPress(widget.sendEmail!), - if (widget.doctorResponse != null) - actionButton(context, t, title: LocaleKeys.doctorResponses.tr(context: context), icon: AppAssets.doctorResponseIcon) - .onPress(widget.doctorResponse!), + if (widget.logout != null) actionButton(context, t, title: LocaleKeys.logout.tr(context: context), icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: LocaleKeys.feedback.tr(context: context), icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: LocaleKeys.history.tr(context: context), icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: LocaleKeys.instructions.tr(context: context), icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: LocaleKeys.requests.tr(context: context), icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.sendEmail != null) actionButton(context, t, title: LocaleKeys.sendEmail.tr(context: context), icon: AppAssets.email).onPress(widget.sendEmail!), + if (widget.doctorResponse != null) actionButton(context, t, title: LocaleKeys.doctorResponses.tr(context: context), icon: AppAssets.doctorResponseIcon).onPress(widget.doctorResponse!), if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!), - if (widget.aiOverview != null) - actionButton(context, t, title: LocaleKeys.aiOverView.tr(context: context), icon: AppAssets.aiOverView, isAiButton: true) - .onPress(widget.aiOverview!), - if (widget.downloadReport != null) - actionButton(context, t, title: LocaleKeys.downloadReport.tr(context: context), icon: AppAssets.download) - .onPress(widget.downloadReport!), - if (widget.viewImage != null) - actionButton(context, t, title: LocaleKeys.viewRadiologyImage.tr(context: context), icon: AppAssets.download) - .onPress(widget.viewImage!), - if (widget.location != null) - actionButton(context, t, title: LocaleKeys.sortByLocation.tr(context: context), icon: AppAssets.location).onPress(widget.location!), - if (widget.downloadInvoice != null) - actionButton(context, t, title: LocaleKeys.downloadInvoice.tr(context: context), icon: AppAssets.download) - .onPress(widget.downloadInvoice!), + if (widget.aiOverview != null) actionButton(context, t, title: LocaleKeys.aiOverView.tr(context: context), icon: AppAssets.aiOverView, isAiButton: true).onPress(widget.aiOverview!), + if (widget.downloadReport != null) actionButton(context, t, title: LocaleKeys.downloadReport.tr(context: context), icon: AppAssets.download).onPress(widget.downloadReport!), + if (widget.viewImage != null) actionButton(context, t, title: LocaleKeys.viewRadiologyImage.tr(context: context), icon: AppAssets.download).onPress(widget.viewImage!), + if (widget.location != null) actionButton(context, t, title: LocaleKeys.sortByLocation.tr(context: context), icon: AppAssets.location).onPress(widget.location!), + if (widget.downloadInvoice != null) actionButton(context, t, title: LocaleKeys.downloadInvoice.tr(context: context), icon: AppAssets.download).onPress(widget.downloadInvoice!), if (widget.trailing != null) widget.trailing!, ] ], @@ -353,8 +332,7 @@ class _ScrollAnimatedTitleState extends State { child: Container( height: isFoldable ? 50.h : 40.h, padding: EdgeInsets.all(8.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, borderRadius: 8.r, side: BorderSide(width: 1, color: AppColors.borderGrayColor)), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 8.r, side: BorderSide(width: 1, color: AppColors.borderGrayColor)), child: ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => AppColors.aiLinearGradient.createShader(bounds), diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 60178939..28098824 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -163,7 +163,7 @@ class _DateRangeSelectorState extends State { ), startRangeSelectionColor: AppColors.primaryRedColor, endRangeSelectionColor: AppColors.primaryRedColor, - rangeSelectionColor: AppColors.primaryRedColorBase.withOpacity(0.1), + rangeSelectionColor: AppColors.secondaryLightRedColor, rangeTextStyle: TextStyle( fontFamily: "Poppins", ), @@ -963,7 +963,7 @@ class _DateRangeSelectorState extends State { children: [ AppCustomChipWidget( labelText: LocaleKeys.thisWeek.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -977,7 +977,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastMonth.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -991,7 +991,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastSixMonths.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -1012,7 +1012,7 @@ class _DateRangeSelectorState extends State { ], ), labelText: "${LocaleKeys.year.tr(context: context)} ${model.getCurrentYear}", - backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, diff --git a/lib/widgets/language_switcher.dart b/lib/widgets/language_switcher.dart index 15237363..7a556c84 100644 --- a/lib/widgets/language_switcher.dart +++ b/lib/widgets/language_switcher.dart @@ -56,21 +56,22 @@ class _LanguageSelectorState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( // currentLangData['name']?.toUpperCase() ?? 'EN', widget.currentLanguage == 'ar' ? "ENGLISH" : "العربية", style: context.dynamicTextStyle( fontWeight: FontWeight.w600, - fontSize: 14.f, - color: AppColors.primaryRedColor, - letterSpacing: 0.1, - isLanguageSwitcher: true, + fontSize: 14.f, + color: AppColors.primaryRedColor, + letterSpacing: 0.1, + isLanguageSwitcher: true, fontFamily: widget.currentLanguage == 'ar' ? "Poppins" : "CairoArabic"), ), SizedBox(width: 6.h), - Utils.buildSvgWithAssets(icon: AppAssets.language), - + Utils.buildSvgWithAssets( + icon: AppAssets.language, + iconColor: AppColors.primaryRedColor, + ), ], ), ), diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index 2994db57..e2059a52 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -220,7 +220,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColorBase.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/lib/widgets/map/gms_map.dart b/lib/widgets/map/gms_map.dart index 1fe7f442..07bf2674 100644 --- a/lib/widgets/map/gms_map.dart +++ b/lib/widgets/map/gms_map.dart @@ -372,7 +372,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColorBase.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/pubspec.lock b/pubspec.lock index 81dfad3e..8b4bdedd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -486,6 +486,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.10.10" + firebase_remote_config: + dependency: "direct main" + description: + name: firebase_remote_config + sha256: e1635b1e8713f4a823920ec3a56a14034b90ce455d47746ab0da994857f370cf + url: "https://pub.dev" + source: hosted + version: "5.5.0" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + sha256: ce836c5c62056edbe23ef501e6876691ee32476afd12fe95b76e57bba9d25485 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: "9dbd75024bfcd47c05046c95f9cf648a8fc862b096bcf8ea1e4b855d50ea19ad" + url: "https://pub.dev" + source: hosted + version: "1.8.9" fixnum: dependency: transitive description: @@ -797,10 +821,10 @@ packages: dependency: "direct main" description: name: flutter_zoom_videosdk - sha256: "46a4dea664b1c969099328a499c198a1755adf9ac333dea28bea5187910b3bf9" + sha256: "87caad07b994f09b9d8c4b8054c0dff5659029abae38dd6ec223460c3f1bcb7d" url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.5.10" fluttertoast: dependency: "direct main" description: @@ -809,6 +833,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.14" + freerasp: + dependency: "direct main" + description: + name: freerasp + sha256: a3d019fa2c1a8f4bb15504d86997f583d1066fe0832172fd68ea6cd521e6fdcc + url: "https://pub.dev" + source: hosted + version: "8.2.2" fwfh_cached_network_image: dependency: transitive description: @@ -1657,6 +1689,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + safe_device: + dependency: "direct main" + description: + name: safe_device + sha256: ad1b32a564484c710f6d52b5a526838c8a7d999c8ab2e14890ff55c06a018248 + url: "https://pub.dev" + source: hosted + version: "1.4.1" sanitize_html: dependency: transitive description: From 803d74ccebc0576df9ec6ee564645864ecf5b4f0 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Tue, 22 Sep 2026 16:50:37 +0300 Subject: [PATCH 71/73] Dynamic theming has been implemented --- lib/core/api/api_client.dart | 97 +++++----- lib/core/api_consts.dart | 3 +- .../appointment_details_page.dart | 4 +- .../appointments/my_appointments_page.dart | 10 +- .../widgets/appointment_doctor_card.dart | 33 ++-- .../widgets/appointment_rating_widget.dart | 6 +- .../authentication/quick_login.dart | 4 +- .../book_appointment_page.dart | 10 +- .../book_appointment/doctor_profile_page.dart | 44 ++--- .../laser/laser_appointment.dart | 4 +- .../book_appointment/widgets/doctor_card.dart | 6 +- lib/presentation/home/landing_page.dart | 25 ++- lib/presentation/lab/alphabeticScroll.dart | 165 ++++++++---------- lib/presentation/lab/lab_order_by_test.dart | 115 ++++++------ lib/presentation/lab/lab_orders_page.dart | 2 +- .../my_invoices/my_invoices_list.dart | 57 +++--- .../onboarding/onboarding_screen.dart | 2 +- .../onboarding/splash_animation_screen.dart | 6 +- .../rate_appointment_clinic.dart | 6 +- .../rate_appointment_doctor.dart | 6 +- .../smartwatches/activity_detail.dart | 4 +- .../health_dashboard/health_dashboard.dart | 4 +- .../widgets/ancillary_orders_list.dart | 6 +- .../widgets/ancillary_procedures_list.dart | 6 +- lib/theme/colors.dart | 2 +- lib/widgets/buttons/custom_button.dart | 14 +- 26 files changed, 289 insertions(+), 352 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 4c961843..5454c2f4 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -138,8 +138,7 @@ class ApiClientImp implements ApiClient { } else {} if (body.containsKey('isDentalAllowedBackend')) { - body['isDentalAllowedBackend'] = - body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND; + body['isDentalAllowedBackend'] = body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND; } if (!body.containsKey('IsPublicRequest')) { @@ -271,11 +270,11 @@ class ApiClientImp implements ApiClient { Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'POST'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_socket_exception'); return; @@ -283,19 +282,19 @@ class ApiClientImp implements ApiClient { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1, failureType: ConnectivityFailure(message)); _analytics.errorTracking.log(endPoint, error: "ClientException: $e"); - + // Enhanced Clarity logging Clarity.setCustomTag('api_err_type', 'client_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'POST'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_client_exception'); - + return; } on TimeoutException catch (_) { final message = LocaleKeys.networkTimeout.tr(); @@ -307,36 +306,36 @@ class ApiClientImp implements ApiClient { Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'POST'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_timeout'); - + return; } catch (e) { final message = LocaleKeys.networkUnknownError.tr(); onFailure(message, -1, failureType: ConnectivityFailure(message)); _analytics.errorTracking.log(endPoint, error: "UnknownException: $e"); - + // Enhanced Clarity logging for unknown errors Clarity.setCustomTag('api_err_type', 'unknown_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'POST'); - + // Truncate exception message for Clarity final exceptionMsg = e.toString(); final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg; Clarity.setCustomTag('api_err_exception', truncatedMsg); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_unknown_exception'); - + return; } @@ -384,24 +383,19 @@ class ApiClientImp implements ApiClient { onFailure( parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode, - failureType: - UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), + failureType: UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), ); // logApiEndpointError(endPoint, "session logged out", statusCode); } if (isAllowAny) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['IsAuthenticated'] == null) { if (parsed['isSMSSent'] == true) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['MessageStatus'] == 1) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['Result'] == 'OK') { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { onFailure( parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], @@ -411,8 +405,7 @@ class ApiClientImp implements ApiClient { logApiEndpointError(endPoint, parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode); } } else if (parsed['MessageStatus'] == 1 || parsed['SMSLoginRequired'] == true) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } // else if (parsed['IsAuthenticated'] == false) { // onFailure( @@ -424,8 +417,7 @@ class ApiClientImp implements ApiClient { // } else if (parsed['MessageStatus'] == 2 && parsed['IsAuthenticated']) { if (parsed['SameClinicApptList'] != null) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { if (parsed['message'] == null && parsed['ErrorEndUserMessage'] == null) { if (parsed['ErrorSearchMsg'] == null) { @@ -459,8 +451,7 @@ class ApiClientImp implements ApiClient { } } else { if (parsed['SameClinicApptList'] != null) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { if (parsed['message'] != null) { onFailure( @@ -537,74 +528,74 @@ class ApiClientImp implements ApiClient { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "SocketException: $e"); - + // Clarity logging for GET requests Clarity.setCustomTag('api_err_type', 'socket_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_socket_exception'); - + return; } on http.ClientException catch (e) { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "ClientException: $e"); - + Clarity.setCustomTag('api_err_type', 'client_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_client_exception'); - + return; } on TimeoutException catch (_) { final message = LocaleKeys.networkTimeout.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "TimeoutException"); - + Clarity.setCustomTag('api_err_type', 'timeout'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_timeout'); - + return; } catch (e) { final message = LocaleKeys.networkUnknownError.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "UnknownException: $e"); - + Clarity.setCustomTag('api_err_type', 'unknown_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + final exceptionMsg = e.toString(); final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg; Clarity.setCustomTag('api_err_exception', truncatedMsg); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_unknown_exception'); - + return; } @@ -614,18 +605,18 @@ class ApiClientImp implements ApiClient { if (statusCode < 200 || statusCode >= 400) { onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode); logApiEndpointError(endPoint, LocaleKeys.networkErrorWhileFetching.tr(), statusCode); - + // Clarity logging for GET status code errors Clarity.setCustomTag('api_err_type', 'status_code_error'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); Clarity.setCustomTag('api_err_status', statusCode.toString()); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_status_error'); } else { var parsed = json.decode(utf8.decode(response.bodyBytes)); @@ -813,7 +804,7 @@ class ApiClientImp implements ApiClient { // Extract the last part of the endpoint path for cleaner tracking const int clarityLimit = 100; final cleanEndpoint = endpoint.split('/').last; - + if (cleanEndpoint.length <= clarityLimit) return cleanEndpoint; return '...${cleanEndpoint.substring(cleanEndpoint.length - (clarityLimit - 3))}'; } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 45d6f48c..39e4a677 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -864,8 +864,7 @@ var GET_CUSTOMER_INFO = "VerifyCustomer"; //Pharmacy -var GET_PHARMACY_CATEGORISE = - 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0'; +var GET_PHARMACY_CATEGORISE = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0'; var GET_OFFERS_CATEGORISE = 'discountcategories'; var GET_OFFERS_PRODUCTS = 'offerproducts/'; var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id='; diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index dc147981..7d1d3f91 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -989,7 +989,7 @@ class _AppointmentDetailsPageState extends State { openDoctorScheduleCalendar(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, @@ -1097,7 +1097,7 @@ class _AppointmentDetailsPageState extends State { openDoctorScheduleCalendar(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index da20e183..e15edb47 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -67,8 +67,8 @@ class _MyAppointmentsPageState extends State { SizedBox(height: 16.h), CustomTabBar( initialIndex: myAppointmentsVM.selectedTabIndex, - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ // CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), @@ -269,9 +269,9 @@ class _MyAppointmentsPageState extends State { ), ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, borderRadius: 12.r, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 4dd838ea..a3b13259 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -84,8 +84,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.h, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), - "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}" - .toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100), @@ -102,11 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Expanded( child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16( - isBold: true, - isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), - textOverflow: TextOverflow.ellipsis, - maxlines: 2 - ), + isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), textOverflow: TextOverflow.ellipsis, maxlines: 2), ), SizedBox(width: 12.w), // (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && @@ -142,18 +137,17 @@ class AppointmentDoctorCard extends StatelessWidget { child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - richText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(patientAppointmentHistoryResponseModel.startTime != null && patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(patientAppointmentHistoryResponseModel.appointmentDate!, patientAppointmentHistoryResponseModel.projectID), false)}" - .toText10(isBold: true), + richText: + "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(patientAppointmentHistoryResponseModel.startTime != null && patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(patientAppointmentHistoryResponseModel.appointmentDate!, patientAppointmentHistoryResponseModel.projectID), false)}" + .toText10(isBold: true), ), ), ), AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon - : AppAssets.small_livecare_icon, + icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, iconColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, - labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) - : LocaleKeys.walkin.tr(context: context), + labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context), backgroundColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.greyColor : AppColors.successColor, textColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, ), @@ -163,13 +157,12 @@ class AppointmentDoctorCard extends StatelessWidget { ), ), (patientAppointmentHistoryResponseModel.isLiveCareAppointment! || - patientAppointmentHistoryResponseModel.isExecludeDoctor! || + patientAppointmentHistoryResponseModel.isExecludeDoctor! || patientAppointmentHistoryResponseModel.isClinicReBookingAllowed! == false || patientAppointmentHistoryResponseModel.isActiveDoctor! == false || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID)) + !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID)) ? SizedBox.shrink() - : Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown) - .onPress(() async { + : Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).onPress(() async { // DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); // selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; // selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; @@ -270,7 +263,7 @@ class AppointmentDoctorCard extends StatelessWidget { onAskDoctorTap(); }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, @@ -310,7 +303,7 @@ class AppointmentDoctorCard extends StatelessWidget { onCancelTap(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: AppColors.whiteColor, fontSize: 14.f, isBold: true, @@ -329,7 +322,7 @@ class AppointmentDoctorCard extends StatelessWidget { onRescheduleTap(); }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, diff --git a/lib/presentation/appointments/widgets/appointment_rating_widget.dart b/lib/presentation/appointments/widgets/appointment_rating_widget.dart index 9b13c6cd..ae70da1d 100644 --- a/lib/presentation/appointments/widgets/appointment_rating_widget.dart +++ b/lib/presentation/appointments/widgets/appointment_rating_widget.dart @@ -142,9 +142,9 @@ class _AppointmentRatingWidgetState extends State { Expanded( child: CustomButton( text: LocaleKeys.later.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { Navigator.pop(context); }, diff --git a/lib/presentation/authentication/quick_login.dart b/lib/presentation/authentication/quick_login.dart index 0830b23e..00e872a3 100644 --- a/lib/presentation/authentication/quick_login.dart +++ b/lib/presentation/authentication/quick_login.dart @@ -114,7 +114,7 @@ class QuickLoginState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Expanded( + Expanded( child: CustomButton( text: LocaleKeys.notNow.tr(context: context), onPressed: () { @@ -122,7 +122,7 @@ class QuickLoginState extends State { Navigator.pop(context, "true"); }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, height: 56.h, // icon: "assets/images/svg/apple-finder.svg", diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index c2560ecd..57b3d541 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -106,8 +106,8 @@ class _BookAppointmentPageState extends State { children: [ SizedBox(height: 16.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, initialIndex: bookAppointmentsVM.selectedTabIndex, tabs: [ CustomTabBarModel(null, LocaleKeys.general.tr(context: context)), @@ -326,9 +326,9 @@ class _BookAppointmentPageState extends State { onPressed: () { Navigator.of(context).push(CustomPageRoute(page: SearchDoctorByName())); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 5b6ad09d..ee4d859b 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -60,9 +60,7 @@ class DoctorProfilePage extends StatelessWidget { doctorID: viewModel.doctorsProfileResponseModel.doctorID ?? 0, isActive: viewModel.isFavouriteDoctor, onSuccess: (response) { - Utils.showToast(viewModel.isFavouriteDoctor - ? LocaleKeys.doctorAddedToFavourite.tr(context: context) - : LocaleKeys.doctorRemovedFromFavourite.tr(context: context)); + Utils.showToast(viewModel.isFavouriteDoctor ? LocaleKeys.doctorAddedToFavourite.tr(context: context) : LocaleKeys.doctorRemovedFromFavourite.tr(context: context)); // Successfully added/removed favorite - refresh the favorites list getIt.get().refreshFavouriteDoctors(); }, @@ -101,14 +99,11 @@ class DoctorProfilePage extends StatelessWidget { children: [ SizedBox( width: isFoldable ? 250.w : 220.w, - child: - ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") - .toString() - .toText24(isBold: true), + child: ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") + .toString() + .toText24(isBold: true), ), - (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty - ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first - : "") + (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first : "") .toString() .toText18(isBold: true, color: AppColors.primaryRedColor), ], @@ -148,16 +143,12 @@ class DoctorProfilePage extends StatelessWidget { children: [ Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_rating_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_rating_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), LocaleKeys.ratings.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor), - bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate.toString().toText16( - isBold: true, - color: AppColors.textColor, - isUnderLine: true, - decorationColor: AppColors.textColor, - fontFamily: "Poppins"), + bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate + .toString() + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); @@ -173,18 +164,12 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(width: 36.w), Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), LocaleKeys.reviews.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor), NumberFormat.decimalPattern("en_US") .format(bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate ?? 0) - .toText16( - isBold: true, - color: AppColors.textColor, - isUnderLine: true, - decorationColor: AppColors.textColor, - fontFamily: "Poppins"), + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); @@ -215,9 +200,7 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(height: 16.h), LocaleKeys.information.tr(context: context).toText14(isBold: true, color: AppColors.textColor), SizedBox(height: 6.h), - (bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo ?? "") - .trim() - .toText12(isBold: true, color: AppColors.greyTextColor), + (bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo ?? "").trim().toText12(isBold: true, color: AppColors.greyTextColor), SizedBox(height: 24.h), ], ).paddingSymmetrical(24.h, 0.h), @@ -238,8 +221,7 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.selectedDoctor.specialityN = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; bookAppointmentsViewModel.selectedDoctor.doctorImageURL = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL; - bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL = - bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL; + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL = bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL; bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription; bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName; diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index c8773933..affd5423 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -80,8 +80,8 @@ class LaserAppointment extends StatelessWidget { child: Column( children: [ CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, LocaleKeys.malE.tr()), CustomTabBarModel(null, LocaleKeys.femaleGender.tr()), diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 23a9cbcd..70f99303 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -308,9 +308,9 @@ class DoctorCard extends StatelessWidget { }); } }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, borderRadius: 10.r, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b7e537f1..a4f194bf 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -291,7 +291,6 @@ class _LandingPageState extends State { child: Column( children: [ const OccasionBannerWidget(), - Row( spacing: 8.h, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -358,11 +357,11 @@ class _LandingPageState extends State { // ...existing commented code... }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w700, - borderRadius: 12.r, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(12.h, 0, 12.h, 0), height: 40.h, ), @@ -685,9 +684,9 @@ class _LandingPageState extends State { getIt.get().onTabChanged(0); Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), @@ -1009,13 +1008,13 @@ class _LandingPageState extends State { ), ), ), - (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) + (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) ? Container( height: MediaQuery.paddingOf(context).top + 50.h, decoration: ShapeDecoration( - color: AppColors.secondaryLightRedBorderColor, + color: AppColors.secondaryLightRedColorBase, shape: SmoothRectangleBorder( - side: BorderSide(width: 1, color: AppColors.primaryRedColor.withAlpha(20)), + side: BorderSide(width: 1, color: AppColors.primaryRedColorBase.withAlpha(20)), // borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), smoothness: 1, ), @@ -1026,7 +1025,7 @@ class _LandingPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true).paddingSymmetrical(0.h, 0.h), + LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColorBase, isBold: true).paddingSymmetrical(0.h, 0.h), Row( children: [ CustomButton( @@ -1038,8 +1037,8 @@ class _LandingPageState extends State { showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.secondaryLightRedBorderColor, + backgroundColor: AppColors.primaryRedColorBase, + borderColor: AppColors.primaryRedColorBase, textColor: Colors.white, fontSize: 12.f, fontWeight: FontWeight.bold, @@ -1047,7 +1046,7 @@ class _LandingPageState extends State { padding: EdgeInsets.fromLTRB(15, 0, 15, 0), height: 36.h, ).paddingSymmetrical(12.h, 0.h), - Icon(Icons.close, color: AppColors.primaryRedColor).onPress(() { + Icon(Icons.close, color: AppColors.primaryRedColorBase).onPress(() { insuranceVM.setIsInsuranceExpiryBannerShown(false); }), ], diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart index ff0efbf4..a1a9add8 100644 --- a/lib/presentation/lab/alphabeticScroll.dart +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -14,8 +14,8 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'lab_order_by_test.dart'; -class AlphabeticScroll extends StatefulWidget{ - final List alpahbetsAvailable; +class AlphabeticScroll extends StatefulWidget { + final List alpahbetsAvailable; final List details; final AppState appState; final LabViewModel labViewModel; @@ -38,13 +38,13 @@ class _AlphabetScrollPageState extends State { Map density = {}; Map _offsetMap = {}; - Map _offsetKeys = {}; + Map _offsetKeys = {}; int _activeIndex = 0; // <-- Highlighted letter @override void initState() { super.initState(); - scheduleMicrotask((){ + scheduleMicrotask(() { _rebuildData(); }); itemPositionsListener.itemPositions.addListener(_onPositionChanged); @@ -61,9 +61,7 @@ class _AlphabetScrollPageState extends State { void _rebuildData() { data.clear(); for (var char in widget.alpahbetsAvailable) { - data[char] = widget.details - .where((element) => element.description?.toLowerCase().startsWith(char.toLowerCase()) == true) - .toList(); + data[char] = widget.details.where((element) => element.description?.toLowerCase().startsWith(char.toLowerCase()) == true).toList(); } if (_activeIndex >= widget.alpahbetsAvailable.length) { _activeIndex = 0; @@ -79,15 +77,12 @@ class _AlphabetScrollPageState extends State { // Get FIRST visible item (top-most) final firstVisible = positions .where((p) => p.itemTrailingEdge > 0) // visible - .reduce((min, p) => - p.itemLeadingEdge < min.itemLeadingEdge ? p : min); + .reduce((min, p) => p.itemLeadingEdge < min.itemLeadingEdge ? p : min); - if(_activeIndex == firstVisible.index) return ; + if (_activeIndex == firstVisible.index) return; setState(() { _activeIndex = firstVisible.index; }); - - print("Active index = $_activeIndex"); } @override @@ -97,12 +92,8 @@ class _AlphabetScrollPageState extends State { } void _scrollToLetter(String letter) async { - // itemScrollController.jumpTo(index:density[letter]?.toInt()??0, ); - itemScrollController.scrollTo( - index: data.keys.toList().indexOf(letter).toInt()??0, - duration: Duration(seconds: 2), - curve: Curves.easeInOutCubic); + itemScrollController.scrollTo(index: data.keys.toList().indexOf(letter).toInt() ?? 0, duration: Duration(seconds: 2), curve: Curves.easeInOutCubic); // final key = _offsetKeys[letter]; // if (key == null) return; // @@ -134,58 +125,52 @@ class _AlphabetScrollPageState extends State { @override Widget build(BuildContext context) { return SizedBox( - width: MediaQuery.sizeOf(context).width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, // Add this - children: [ - Expanded( - child: Column( - children: [ - SizedBox( - height: (MediaQuery.sizeOf(context).height), - child: _buildList(widget.alpahbetsAvailable) - ), - ], - ), + width: MediaQuery.sizeOf(context).width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, // Add this + children: [ + Expanded( + child: Column( + children: [ + SizedBox(height: (MediaQuery.sizeOf(context).height), child: _buildList(widget.alpahbetsAvailable)), + ], ), - SizedBox( - width: 24.w, - height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h), - child: Column( - mainAxisSize: MainAxisSize.max, // Add this - mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start - crossAxisAlignment: CrossAxisAlignment.center, - children:List.generate(widget.alpahbetsAvailable.length, (i) { - final isActive = (i == _activeIndex); - return GestureDetector( - onTap: () { - setState(() => _activeIndex = i); - _scrollToLetter(widget.alpahbetsAvailable[i]); - }, - child: TweenAnimationBuilder( - tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), - duration: Duration(milliseconds: 120), - curve: Curves.easeOut, - builder: (_, scale, child) { - return Transform.scale( - scale: scale, - child: Opacity( - opacity: isActive ? 1.0 : 0.5, - child: widget.alpahbetsAvailable[i].toUpperCase().toText14( - color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor, isEnglishOnly: true - ), - ), - ); - }, - ), - ); - } + ), + SizedBox( + width: 24.w, + height: MediaQuery.sizeOf(context).height - (70.h + kToolbarHeight + 100.h), + child: Column( + mainAxisSize: MainAxisSize.max, // Add this + mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(widget.alpahbetsAvailable.length, (i) { + final isActive = (i == _activeIndex); + return GestureDetector( + onTap: () { + setState(() => _activeIndex = i); + _scrollToLetter(widget.alpahbetsAvailable[i]); + }, + child: TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), + duration: Duration(milliseconds: 120), + curve: Curves.easeOut, + builder: (_, scale, child) { + return Transform.scale( + scale: scale, + child: Opacity( + opacity: isActive ? 1.0 : 0.5, + child: widget.alpahbetsAvailable[i].toUpperCase().toText14(color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor, isEnglishOnly: true), + ), + ); + }, ), - ), - ), - ], - ), - ); + ); + }), + ), + ), + ], + ), + ); // ); } @@ -205,35 +190,35 @@ class _AlphabetScrollPageState extends State { key: _offsetKeys[letter], child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: - List.generate(items.length,(itemIndex)=> - AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: LabOrderByTest( - appState: getIt(), - onTap: () { - if (items[itemIndex].model != null) { - widget.rangeViewModel.flush(); - widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, - (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!),""); - } - }, - tests: items[itemIndex], - index: itemIndex, - isExpanded: true)), - ), - )) + children: List.generate( + items.length, + (itemIndex) => AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: LabOrderByTest( + appState: getIt(), + onTap: () { + if (items[itemIndex].model != null) { + widget.rangeViewModel.flush(); + widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, + (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!), ""); + } + }, + tests: items[itemIndex], + index: itemIndex, + isExpanded: true)), + ), + )) // ...items.indexed((item) => // // // // ) - ), + ), ); }, ); diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 7740a596..733fb792 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -26,64 +26,65 @@ class LabOrderByTest extends StatelessWidget { @override build(BuildContext context) { return AnimatedContainer( - key:key, - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - '${tests!.description}'.toText16(isBold: true, isEnglishOnly: true), - SizedBox(height: 4.h), - (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(isBold: true), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AppCustomChipWidget( - richText: Row( - children: [ - "${LocaleKeys.lastTested.tr(context: context)}: ".toText12(isBold: true), - Directionality( - textDirection: ui.TextDirection.ltr,child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false).toText12(isBold: true, isEnglishOnly: true)) - ], - ), - backgroundColor: AppColors.greyLightColor, - textColor: AppColors.textColor, + key: key, + 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: Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + '${tests!.description}'.toText16(isBold: true, isEnglishOnly: true), + SizedBox(height: 4.h), + (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(isBold: true), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + richText: Row( + children: [ + "${LocaleKeys.lastTested.tr(context: context)}: ".toText12(isBold: true), + Directionality( + textDirection: ui.TextDirection.ltr, child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false).toText12(isBold: true, isEnglishOnly: true)) + ], ), - ], - ), - SizedBox(height: 16.h), - Row( - children: [ - Expanded(child: Container()), - Expanded( - child: CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewResults.tr(context: context), - onPressed: () { - onTap(); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), + backgroundColor: AppColors.greyLightColor, + textColor: AppColors.textColor, + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded(child: Container()), + Expanded( + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: LocaleKeys.viewResults.tr(context: context), + onPressed: () { + onTap(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, ), - ], - ), - ], - ), - )); + ), + ], + ), + ], + ), + ), + ); } } diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 0e8890ba..332a89da 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), 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 +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: AppColors.primaryRedColor, activeBackgroundColor: AppColors.secondaryLightRedColor, 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/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart index 67ee05a6..95ff3e16 100644 --- a/lib/presentation/my_invoices/my_invoices_list.dart +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -122,8 +122,8 @@ class _MyInvoicesListState extends State { children: [ SizedBox(height: 16.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, initialIndex: 0, tabs: [ CustomTabBarModel(null, LocaleKeys.visitsOrders.tr(context: context)), @@ -149,13 +149,11 @@ class _MyInvoicesListState extends State { myInvoicesViewModel.filterInvoices(InvoiceFilterType.all); }, backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -166,15 +164,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showHospitalFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -185,15 +180,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showClinicFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -204,15 +196,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showDoctorFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -245,16 +234,14 @@ class _MyInvoicesListState extends State { getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], onTap: () async { if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) { - LoaderBottomSheet.showLoader( - loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); await myInvoicesViewModel.sendInvoiceEmail( appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, projectID: myInvoicesVM.allInvoicesList[index].projectId!, onSuccess: (val) { LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, - child: Utils.getSuccessWidget( - loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), + child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, @@ -289,8 +276,7 @@ class _MyInvoicesListState extends State { onSuccess: (value) async { LoaderBottomSheet.hideLoader(); if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString( - myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); + String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); try { OpenFilex.open(path); } catch (ex) { @@ -377,8 +363,7 @@ class _MyInvoicesListState extends State { onSuccess: (value) async { LoaderBottomSheet.hideLoader(); if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString( - myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); + String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); try { OpenFilex.open(path); } catch (ex) { diff --git a/lib/presentation/onboarding/onboarding_screen.dart b/lib/presentation/onboarding/onboarding_screen.dart index 0af63b40..1c5b64d8 100644 --- a/lib/presentation/onboarding/onboarding_screen.dart +++ b/lib/presentation/onboarding/onboarding_screen.dart @@ -111,7 +111,7 @@ class _OnboardingScreenState extends State { onPressed: () => goToHomePage(), width: 86.w, height: 56.h, - backgroundColor: Color(0xffFEE9EA), + backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, borderColor: Colors.transparent, ).paddingOnly(left: 24.w) diff --git a/lib/presentation/onboarding/splash_animation_screen.dart b/lib/presentation/onboarding/splash_animation_screen.dart index 105922bb..0fdd86a8 100644 --- a/lib/presentation/onboarding/splash_animation_screen.dart +++ b/lib/presentation/onboarding/splash_animation_screen.dart @@ -164,7 +164,7 @@ class _AnimatedScreenState extends State with TickerProviderStat width: 400.h, height: 653.h, decoration: BoxDecoration( - color: Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330.h), ), ), @@ -273,7 +273,7 @@ class _MoveObjectDemoState extends State with SingleTickerProvid width: 200, height: 375, decoration: BoxDecoration( - color: const Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330), ), ), @@ -367,7 +367,7 @@ class _MoveOnClickDemoState extends State with TickerProviderSt width: 100, height: 150, decoration: BoxDecoration( - color: const Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330), ), ), diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart index c8aedea8..e285a7c1 100644 --- a/lib/presentation/rate_appointment/rate_appointment_clinic.dart +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -159,9 +159,9 @@ class _RateAppointmentClinicState extends State { Expanded( child: CustomButton( text: LocaleKeys.back.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { appointmentRatingViewModel!.setTitle(LocaleKeys.rateDoctor.tr(context: context)); appointmentRatingViewModel!.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart index d78dc59e..b54b5741 100644 --- a/lib/presentation/rate_appointment/rate_appointment_doctor.dart +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -181,9 +181,9 @@ class _RateAppointmentDoctorState extends State { Expanded( child: CustomButton( text: LocaleKeys.later.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { Navigator.pop(context); }, diff --git a/lib/presentation/smartwatches/activity_detail.dart b/lib/presentation/smartwatches/activity_detail.dart index 7911baf6..c246d5a0 100644 --- a/lib/presentation/smartwatches/activity_detail.dart +++ b/lib/presentation/smartwatches/activity_detail.dart @@ -66,8 +66,8 @@ class _ActivityDetailsState extends State { children: [ Expanded( child: CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, "D"), CustomTabBarModel(null, "W"), diff --git a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart index f30d87a9..84ec64e9 100644 --- a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart +++ b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart @@ -56,8 +56,8 @@ class _HealthDashboardState extends State with SingleTickerProv children: [ SizedBox(height: 96.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, "Overview".needTranslation), CustomTabBarModel(null, "Details".needTranslation), diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index cc5bbbb6..da75dd7b 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -257,9 +257,9 @@ class AncillaryOrderCard extends StatelessWidget { onViewDetails!(); } }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w600, borderRadius: 10.r, diff --git a/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart index 54c92789..b7f0f28a 100644 --- a/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart @@ -252,9 +252,9 @@ class AncillaryOrderCard extends StatelessWidget { onViewDetails!(); } }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, borderRadius: 10.r, diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 24c8bd25..3b9cbdae 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -54,7 +54,7 @@ class AppColors { /// The original, non-themed brand red (or its dark-mode variant). Always /// stable regardless of any active occasion theme — intended for opacity /// derived usages (e.g. `AppColors.primaryRedColorBase.withOpacity(0.1)`). - static Color get primaryRedColorBase => isDarkMode ? const Color(0xFFED1C2B) : const Color(0xFFED1C2B); + static Color get primaryRedColorBase => isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B); /// Semantic color for critical/negative actions (delete/deactivate account, /// cancel appointment, clear/reset, emergency, etc.). Same light/dark diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 82b62507..8b8e599a 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -10,8 +10,8 @@ class CustomButton extends StatefulWidget { final String text; final String? icon; final VoidCallback? onPressed; - final Color backgroundColor; - final Color borderColor; + final Color? backgroundColor; + final Color? borderColor; final Color textColor; final double? borderRadius; final double borderWidth; @@ -33,8 +33,8 @@ class CustomButton extends StatefulWidget { super.key, required this.text, required this.onPressed, - this.backgroundColor = const Color(0xFFED1C2B), - this.borderColor = const Color(0xFFED1C2B), + this.backgroundColor, + this.borderColor, this.textColor = Colors.white, this.borderRadius, this.borderWidth = 2, @@ -93,6 +93,8 @@ class _CustomButtonState extends State with SingleTickerProviderSt final iconS = widget.iconSize ?? (24.h); final fontS = widget.fontSize ?? (16.f); final isButtonDisabled = widget.isDisabled || widget.onPressed == null; + final resolvedBackgroundColor = widget.backgroundColor ?? AppColors.primaryRedColor; + final resolvedBorderColor = widget.borderColor ?? AppColors.primaryRedColor; return Semantics( button: true, @@ -134,10 +136,10 @@ class _CustomButtonState extends State with SingleTickerProviderSt width: widget.width, padding: widget.padding, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : widget.backgroundColor, + color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : resolvedBackgroundColor, borderRadius: radius, customBorder: BorderRadius.circular(radius), - side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : widget.borderColor)), + side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : resolvedBorderColor)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, From eb67f39ee23c9ac1653924331b99a1252a201a83 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 22 Sep 2026 16:57:42 +0300 Subject: [PATCH 72/73] Updates --- .../myfatoorah/myfatoorah_view_model.dart | 30 +++++++++++-------- .../immediate_livecare_payment_page.dart | 7 ++++- .../home/widgets/habib_wallet_card.dart | 2 +- .../home/widgets/lakum_wallet_card.dart | 2 +- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/features/myfatoorah/myfatoorah_view_model.dart b/lib/features/myfatoorah/myfatoorah_view_model.dart index fb7a23fa..b76b251e 100644 --- a/lib/features/myfatoorah/myfatoorah_view_model.dart +++ b/lib/features/myfatoorah/myfatoorah_view_model.dart @@ -133,21 +133,27 @@ class MyFatoorahViewModel extends ChangeNotifier { }); } - initApplePayPayment(num invoiceValue, MFApplePayButton mfApplePayButton) async { + executeApplePayPayment() { + MFApplepay.executeApplePayPayment(onInvoiceCreated: (invoiceID) { + log( + invoiceID.toString(), + ); + }).then((value) { + log( + value.toString(), + ); + }).catchError((error) { + log(error.message); + }); + } + + initApplePayPayment(num invoiceValue) async { await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); await MFSDK.initSession(MFInitiateSessionRequest(), MFLanguage.ENGLISH).then((value) async { - var executePaymentRequest = MFExecutePaymentRequest(invoiceValue: 121, displayCurrencyIso: MFCurrencyISO.KUWAIT_KWD); - - await mfApplePayButton.executeApplePayButton( - executePaymentRequest, - (String invoiceId) { - print("Payment Success! Invoice ID: $invoiceId"); - }, - ).catchError((error) { - // If initialized properly, this error won't occur - print("Apple Pay Error: ${error.message}"); - }); + var executePaymentRequest = MFExecutePaymentRequest(invoiceValue: invoiceValue, sessionId: value.sessionId, displayCurrencyIso: MFCurrencyISO.SAUDIARABIA_SAR); + + MFApplepay.setupApplePay(value, executePaymentRequest, MFLanguage.ENGLISH, merchantName: "Dr Sulaiman Al Habib"); }).catchError((error) { log(error.message); }); 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 40a79370..2147cbd6 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -71,6 +71,11 @@ class _ImmediateLiveCarePaymentPageState extends State= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && @@ -755,7 +760,7 @@ class _ImmediateLiveCarePaymentPageState extends State Date: Tue, 22 Sep 2026 17:35:51 +0300 Subject: [PATCH 73/73] updates --- .../home/widgets/occasion_banner_widget.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart index 646cb70c..1f19b826 100644 --- a/lib/presentation/home/widgets/occasion_banner_widget.dart +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -56,8 +56,6 @@ class OccasionBackgroundWidget extends StatelessWidget { final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); - log("occasionThemeVm.backgroundImageOpacity: ${occasionThemeVm.backgroundImageOpacity}"); - if (!hasBackgroundImage) { return const SizedBox.shrink(); } @@ -65,7 +63,7 @@ class OccasionBackgroundWidget extends StatelessWidget { return Positioned.fill( child: Opacity( opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), - child: buildOccasionImage(backgroundImageUrl!), + child: buildOccasionImage(backgroundImageUrl!, isNeedPadding: true), ), ); }, @@ -77,24 +75,23 @@ class OccasionBackgroundWidget extends StatelessWidget { /// Day banner) or a raster image (PNG/JPG). [CachedNetworkImage] cannot /// decode SVG — it silently renders nothing — so SVG URLs are routed to /// [SvgPicture.network] instead, based on the URL's file extension. -Widget buildOccasionImage(String url) { +Widget buildOccasionImage(String url, {bool isNeedPadding = false}) { final isSvg = url.split('?').first.toLowerCase().endsWith('.svg'); if (isSvg) { return SvgPicture.network( url, width: double.infinity, - fit: BoxFit.cover, + fit: BoxFit.contain, placeholderBuilder: (_) => const SizedBox.shrink(), ); } return CachedNetworkImage( imageUrl: url, width: double.infinity, - fit: BoxFit.cover, + fit: BoxFit.contain, placeholder: (_, __) => const SizedBox.shrink(), errorWidget: (_, __, error) { - log('OccasionImage: failed to load $url -> $error'); return const SizedBox.shrink(); }, - ); + ).paddingSymmetrical(isNeedPadding ? 24.w : 0, 0); }