From 8fad8999d7dfd77a910213d281ccf01de3f5cc15 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 13:45:23 +0300 Subject: [PATCH 1/6] 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 19:22:44 +0300 Subject: [PATCH 2/6] 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, -- 2.30.2 From 20c47d6af095554bfc2fc7b3db65b0c1042dcc37 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 19:24:37 +0300 Subject: [PATCH 3/6] 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 -- 2.30.2 From dc6b71a4ca84a00e40e6b0a3accd79c78931718e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 20:52:58 +0300 Subject: [PATCH 4/6] 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(); } -- 2.30.2 From 0043e2917de089adf966cb226725e9937ac40c89 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 10 Aug 2026 21:12:57 +0300 Subject: [PATCH 5/6] 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" -- 2.30.2 From 0dea7414d92dbff7d00f4cc4c10f3086623f89e4 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 12 Aug 2026 14:21:43 +0300 Subject: [PATCH 6/6] 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 -- 2.30.2