From 2532d958958af392ca2ef18a21786b288feaaf2a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 9 Aug 2026 10:37:32 +0300 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 ddda77c95a9c231ab91ef1ac2c6c35d04a590bee Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Mon, 10 Aug 2026 18:03:55 +0300 Subject: [PATCH 5/5] 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,