diff --git a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart index a97f0dd5..ca8cba71 100644 --- a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart +++ b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart @@ -465,6 +465,9 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { notifyListeners(); } + /// Check if search has text (for showing/hiding clear button) + bool get hasSearchText => searchQuery.isNotEmpty; + /// Get unique categories from the offers list List getUniqueCategories() { if (offersList.isEmpty) return []; diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 7b1547db..12ac518d 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -146,6 +146,9 @@ class _LandingPageState extends State { await authVM.handleNewImeiRegistration(isNavigate: false); } + // Load offers and discounts for all users (authenticated or not) + offersAndDiscountsViewModel.initOffersAndDiscounts(); + if (appState.isAuthenticated) { Clarity.setCustomUserId(appState.getAuthenticatedUser()!.patientId.toString()); @@ -161,7 +164,6 @@ class _LandingPageState extends State { // myAppointmentsViewModel.getPatientAppointmentQueueDetails(); notificationsViewModel.initNotificationsViewModel(); insuranceViewModel.initInsuranceProvider(); - offersAndDiscountsViewModel.initOffersAndDiscounts(); if (!appState.isRatedVisible) { appointmentRatingViewModel.getLastRatingAppointment( @@ -212,13 +214,17 @@ class _LandingPageState extends State { RefreshIndicator( color: AppColors.primaryRedColor, onRefresh: () async { + // Refresh Offers and Discounts for all users + offersAndDiscountsViewModel.setIsoffersListLoading(true); + offersAndDiscountsViewModel.initOffersAndDiscounts(forceRefresh: true); + if (appState.isAuthenticated) { // Refresh Appointments Data myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false); - // Refresh Appointments Data + // Refresh Wallet Data habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); habibWalletVM.getLakumAccountInformation(); @@ -230,8 +236,6 @@ class _LandingPageState extends State { // Refresh Immediate LiveCare Data immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); - offersAndDiscountsViewModel.setIsoffersListLoading(true); - offersAndDiscountsViewModel.initOffersAndDiscounts(forceRefresh: true); // appointmentRatingViewModel.getLastRatingAppointment( // onSuccess: (response) { @@ -406,54 +410,7 @@ class _LandingPageState extends State { }), ], ).paddingSymmetrical(24.h, 0.h), - !appState.isAuthenticated - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - width: 50.w, - height: 60.h, - icon: AppAssets.symptomCheckerIcon, - fit: BoxFit.contain, - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.howAreYouFeelingToday.tr(context: context).toText14(isBold: true), - LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12(isBold: true), - SizedBox(height: 14.h), - CustomButton( - text: LocaleKeys.checkYourSymptoms.tr(context: context), - onPressed: () async { - context.navigateWithName(AppRoutes.userInfoSelection); - }, - padding: EdgeInsetsGeometry.zero, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - height: 40.h, - ), - ], - ).expanded - ], - ), - ), - ).paddingSymmetrical(24.w, 0.h) - : SizedBox.shrink(), - // Offers And Discounts Carousel - Auto-scrolling from right to left - //appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel ? Consumer( builder: (context, offersVM, child) { @@ -506,45 +463,6 @@ class _LandingPageState extends State { separatorBuilder: (context, index) => SizedBox(width: 12.w), ), ).paddingSymmetrical(24.h, 0.h); - - ///////// - - //////////// - // return Column( - // children: [ - // SizedBox(height: 12.h), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), - // Row( - // children: [ - // LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), - // SizedBox(width: 2.h), - // Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), - // ], - // ), - // ], - // ).paddingSymmetrical(24.h, 0.h).onPress(() { - // Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage())); - // }), - // SizedBox(height: 16.h), - // Container( - // height: 150.h, - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.whiteColor, - // borderRadius: 16.r, - // hasShadow: false, - // ), - // child: Center( - // child: CircularProgressIndicator( - // valueColor: AlwaysStoppedAnimation(AppColors.primaryRedColor), - // ), - // ), - // ).paddingSymmetrical(24.h, 0.h), - // SizedBox(height: 18.h), - // ], - // ); } // Show offers if available @@ -568,8 +486,7 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage())); }), SizedBox(height: 16.h), - OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h), - SizedBox(height: 18.h), + OffersAndDiscountsCarousel().paddingOnly(left: 24.w), ], ); } @@ -580,10 +497,58 @@ class _LandingPageState extends State { ) : SizedBox.shrink(), + !appState.isAuthenticated + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + width: 50.w, + height: 60.h, + icon: AppAssets.symptomCheckerIcon, + fit: BoxFit.contain, + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.howAreYouFeelingToday.tr(context: context).toText14(isBold: true), + LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12(isBold: true), + SizedBox(height: 14.h), + CustomButton( + text: LocaleKeys.checkYourSymptoms.tr(context: context), + onPressed: () async { + context.navigateWithName(AppRoutes.userInfoSelection); + }, + padding: EdgeInsetsGeometry.zero, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 40.h, + ), + ], + ).expanded + ], + ), + ), + ).paddingSymmetrical(24.w, 0.h) + : SizedBox.shrink(), + + // Offers And Discounts Carousel - Auto-scrolling from right to left + //appState.isAuthenticated && + appState.isAuthenticated ? Column( children: [ - SizedBox(height: 12.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart index 3d51df67..17f3707f 100644 --- a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart +++ b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart @@ -12,9 +12,11 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -117,37 +119,46 @@ class _OffersAndDiscountsDetailedPageState extends State(context, listen: false); return CollapsingListView( - title: "${LocaleKeys.offerAndPackageDetails.tr(context: context)}", + title: LocaleKeys.offerAndPackageDetails.tr(context: context), bottomChild: CustomButton( text: LocaleKeys.buyNow.tr(context: context), icon: AppAssets.shoppingCart, iconColor: AppColors.whiteColor, onPressed: () async { - // Ensure patient is authenticated (will re-authenticate if needed) - final isAuthenticated = await viewModel.ensureAuthenticated( - onSuccess: () { - // Authentication successful, continue - }, - onError: (error) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); + if (!appState.isAuthenticated) { + getIt().showErrorBottomSheet( + message: LocaleKeys.loginToUseService.tr(), + onOkPressed: () { + Navigator.of(context).pop(); + getIt().onLoginPressed(); + }); + } else { + // Ensure patient is authenticated (will re-authenticate if needed) + final isAuthenticated = await viewModel.ensureAuthenticated( + onSuccess: () { + // Authentication successful, continue + }, + onError: (error) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); - if (!isAuthenticated) { - return; + if (!isAuthenticated) { + return; + } + viewModel.setSelectedOffer(viewModel.offerDetails!); + Navigator.of(context).push( + CustomPageRoute( + page: OffersAndDiscountPaymentPage(), + ), + ); } - viewModel.setSelectedOffer(viewModel.offerDetails!); - Navigator.of(context).push( - CustomPageRoute( - page: OffersAndDiscountPaymentPage(), - ), - ); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, @@ -287,8 +298,12 @@ class _OffersAndDiscountsDetailedPageState extends State { late OffersAndDiscountsViewModel offersAndDiscountsViewModel; late AppState appState; final TextEditingController _searchController = TextEditingController(); + FocusNode textFocusNode = FocusNode(); @override void initState() { super.initState(); appState = getIt.get(); + scheduleMicrotask(() async { offersAndDiscountsViewModel = Provider.of(context, listen: false); @@ -63,7 +67,7 @@ class _OffersAndDiscountsPageState extends State { onError: (error) { // Hide loader on error LoaderBottomSheet.hideLoader(); - print("POMS Authentication failed: $error"); + print("POMS POMS Authentication failed: $error"); // Authentication failed, but user can still browse offers }, ); @@ -166,9 +170,18 @@ class _OffersAndDiscountsPageState extends State { return CollapsingListView( title: LocaleKeys.offerAndPackages.tr(context: context), history: () { - Navigator.of(context).push( - CustomPageRoute(page: OffersAndDiscountsHistoryPage()), - ); + if (!appState.isAuthenticated) { + getIt().showErrorBottomSheet( + message: LocaleKeys.loginToUseService.tr(), + onOkPressed: () { + Navigator.of(context).pop(); + getIt().onLoginPressed(); + }); + } else { + Navigator.of(context).push( + CustomPageRoute(page: OffersAndDiscountsHistoryPage()), + ); + } }, child: SingleChildScrollView( child: Consumer(builder: (context, offersAndDiscountVM, child) { @@ -180,18 +193,29 @@ class _OffersAndDiscountsPageState extends State { TextInputWidget( labelText: LocaleKeys.search.tr(context: context), hintText: LocaleKeys.typeAnyOffer.tr(), - hintColor: AppColors.textColor, controller: _searchController, - onChange: (value) { - offersAndDiscountVM.setSearchQuery(value!); - }, + leadingIcon: AppAssets.search_icon, isEnable: true, prefix: null, + isAllowLeadingIcon: true, autoFocus: false, isBorderAllowed: false, keyboardType: TextInputType.text, - isAllowLeadingIcon: true, - selectionType: SelectionTypeEnum.search, + focusNode: textFocusNode, + suffix: _searchController.text.isNotEmpty + ? GestureDetector( + onTap: () { + _searchController.clear(); + // bookAppointmentsViewModel.filterClinics(""); + offersAndDiscountVM.setSearchQuery(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + offersAndDiscountVM.setSearchQuery(value!); + }, padding: EdgeInsets.symmetric( vertical: ResponsiveExtension(10).h, horizontal: ResponsiveExtension(15).h, @@ -342,49 +366,58 @@ class _OffersAndDiscountsPageState extends State { icon: AppAssets.shoppingCart, iconColor: AppColors.whiteColor, onPressed: () async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context)); + if (!appState.isAuthenticated) { + getIt().showErrorBottomSheet( + message: LocaleKeys.loginToUseService.tr(), + onOkPressed: () { + Navigator.of(context).pop(); + getIt().onLoginPressed(); + }); + } else { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context)); - // Ensure patient is authenticated (will re-authenticate if needed) - final isAuthenticated = await offersAndDiscountVM.ensureAuthenticated( - onSuccess: () { - // Authentication successful, continue - }, - onError: (error) { + // Ensure patient is authenticated (will re-authenticate if needed) + final isAuthenticated = await offersAndDiscountVM.ensureAuthenticated( + onSuccess: () { + // Authentication successful, continue + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + // Show error to user + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + + if (!isAuthenticated) { LoaderBottomSheet.hideLoader(); - // Show error to user - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); + return; + } - if (!isAuthenticated) { - LoaderBottomSheet.hideLoader(); - return; + // Fetch offer details + await offersAndDiscountVM.getOfferDetails( + offersAndDiscountVM.filteredOffers[index].id!, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + if (offersAndDiscountVM.offerDetails != null) { + offersAndDiscountVM.setSelectedOffer(offersAndDiscountVM.offerDetails!); + Navigator.of(context).push( + CustomPageRoute( + page: OffersAndDiscountPaymentPage(), + ), + ); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + }, + ); } - - // Fetch offer details - await offersAndDiscountVM.getOfferDetails( - offersAndDiscountVM.filteredOffers[index].id!, - onSuccess: (response) { - LoaderBottomSheet.hideLoader(); - if (offersAndDiscountVM.offerDetails != null) { - offersAndDiscountVM.setSelectedOffer(offersAndDiscountVM.offerDetails!); - Navigator.of(context).push( - CustomPageRoute( - page: OffersAndDiscountPaymentPage(), - ), - ); - } - }, - onError: (error) { - LoaderBottomSheet.hideLoader(); - }, - ); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index b7fe686f..fc815de3 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -48,6 +48,8 @@ class TextInputWidget extends StatelessWidget { final Color? labelColor; final Function(String)? onSubmitted; final String? btnTitle; + final bool showSearchClear; + final VoidCallback? onClear; // new multiline options final bool isMultiline; @@ -102,6 +104,8 @@ class TextInputWidget extends StatelessWidget { // this.countryList = const [], // this.onCountryChange, this.btnTitle, + this.showSearchClear = false, + this.onClear, }); final FocusNode _focusNode = FocusNode(); @@ -166,42 +170,42 @@ class TextInputWidget extends StatelessWidget { if (isAllowLeadingIcon && leadingIcon != null && !isCountryDropDown) _buildLeadingIcon(context), isCountryDropDown ? CustomCountryDropdown( - countryList: CountryEnum.values, - onCountryChange: onCountryChange, - isFromBottomSheet: isCountryDropDown, - isEnableTextField: true, - onPhoneNumberChanged: onChange, - // textField: _buildTextField(context), - ) + countryList: CountryEnum.values, + onCountryChange: onCountryChange, + isFromBottomSheet: isCountryDropDown, + isEnableTextField: true, + onPhoneNumberChanged: onChange, + // textField: _buildTextField(context), + ) : Expanded( - child: Row( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildLabelText(labelColor).paddingOnly( - right: (appState.isArabic() ? 10 : 10), - left: (!appState.isArabic() ? 10 : 10), - ), - Row( - children: [ - Expanded( - child: _buildTextField(context).paddingOnly( - right: (appState.getLanguageCode() == "ar" ? 10 : 0), - left: (appState.getLanguageCode() == "en" ? 10 : 0), - ), - ) - ], + _buildLabelText(labelColor).paddingOnly( + right: (appState.isArabic() ? 10 : 10), + left: (!appState.isArabic() ? 10 : 10), + ), + Row( + children: [ + Expanded( + child: _buildTextField(context).paddingOnly( + right: (appState.getLanguageCode() == "ar" ? 10 : 0), + left: (appState.getLanguageCode() == "en" ? 10 : 0), ), - ], - ), + ) + ], ), - (suffix != null) ? suffix! : SizedBox.shrink() ], ), ), + (suffix != null) ? suffix! : SizedBox.shrink() + ], + ), + ), if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context, isArrowTrailing: isArrowTrailing ?? false), if (selectionType == SelectionTypeEnum.time) _buildTimePickerIcon(context, isArrowTrailing: isArrowTrailing ?? false), if (selectionType == SelectionTypeEnum.search) _buildTrailingIconForSearch(context), @@ -253,7 +257,10 @@ class TextInputWidget extends StatelessWidget { context, child: DateRangeSelector( onRangeSelected: (DateTime? start, DateTime? end) { - var picked = start.toString().split(" ").first; + var picked = start + .toString() + .split(" ") + .first; if (picked != null && onChange != null) { onChange!(picked.toString()); } @@ -427,8 +434,8 @@ class TextInputWidget extends StatelessWidget { maxLength: isWalletAmountInput! ? 7 : 100, inputFormatters: isWalletAmountInput! ? [ - _ThousandSeparatorInputFormatter(), - ] + _ThousandSeparatorInputFormatter(), + ] : null, onTapOutside: (event) { FocusManager.instance.primaryFocus?.unfocus(); @@ -521,23 +528,39 @@ class TextInputWidget extends StatelessWidget { } _buildTrailingIconForSearch(BuildContext context) { - return Container( - height: 40.h, - width: 40.h, - margin: EdgeInsets.zero, - padding: EdgeInsets.all(8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.whiteColor), - child: Utils.buildSvgWithAssets(icon: AppAssets.search_icon), - ); + if (showSearchClear) { + return GestureDetector( + onTap: () { + if (onClear != null) { + onClear!(); + } + }, + child: Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.zero, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.whiteColor), + child: Utils.buildSvgWithAssets(icon: AppAssets.cross_circle), + ), + ); + } else { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.zero, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.whiteColor), + child: Utils.buildSvgWithAssets(icon: AppAssets.search_icon), + ); + } } } class _ThousandSeparatorInputFormatter extends TextInputFormatter { @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { + TextEditingValue formatEditUpdate(TextEditingValue oldValue, + TextEditingValue newValue,) { // Remove all commas to get the raw number String newText = newValue.text.replaceAll(',', ''); @@ -570,7 +593,11 @@ class _ThousandSeparatorInputFormatter extends TextInputFormatter { count = 0; } } - integerPart = buffer.toString().split('').reversed.join(); + integerPart = buffer + .toString() + .split('') + .reversed + .join(); } final formatted = '$integerPart$decimalPart'; @@ -578,9 +605,13 @@ class _ThousandSeparatorInputFormatter extends TextInputFormatter { // Calculate new cursor position int cursorOffset = newValue.selection.baseOffset; // Count commas before cursor in the new formatted string - int commasInNew = ','.allMatches(formatted.substring(0, cursorOffset.clamp(0, formatted.length))).length; + int commasInNew = ',' + .allMatches(formatted.substring(0, cursorOffset.clamp(0, formatted.length))) + .length; // Count commas before cursor in the old value - int commasInOld = ','.allMatches(oldValue.text.substring(0, oldValue.selection.baseOffset.clamp(0, oldValue.text.length))).length; + int commasInOld = ',' + .allMatches(oldValue.text.substring(0, oldValue.selection.baseOffset.clamp(0, oldValue.text.length))) + .length; int newCursorPos = cursorOffset + (commasInNew - commasInOld); newCursorPos = newCursorPos.clamp(0, formatted.length);