From ea124325f07e13e2c8a824fe578de791afab835b Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Wed, 8 Jul 2026 14:46:31 +0300 Subject: [PATCH] Offers And Discounts Complete --- .../offers_and_discounts_view_model.dart | 30 ++++ lib/presentation/home/landing_page.dart | 4 +- ...and_discounts_success_payment_qr_page.dart | 51 +++++-- .../offers_and_discount_payment_page.dart | 31 ++-- .../offers_and_discounts_detailed_page.dart | 132 +++++++++++------- .../offers_and_discounts_history_page.dart | 53 +++++-- .../offers_and_discounts_page.dart | 34 ++++- 7 files changed, 235 insertions(+), 100 deletions(-) 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 e7cab417..a97f0dd5 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 @@ -25,6 +25,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { bool isAuthenticating = false; bool isPurchasing = false; bool isSaveQrCodePressed = false; // Track QR code capture state + bool hasOfferDetailsLoadingError = false; // Track offer details loading error List selectedCategories = []; // Empty means 'All Offers' String searchQuery = ''; @@ -235,6 +236,32 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { notifyListeners(); } + setHasOfferDetailsLoadingError(bool val) { + hasOfferDetailsLoadingError = val; + notifyListeners(); + } + + /// Reset offer details state (called when navigating to a new offer) + resetOfferDetailsState() { + offerDetails = null; + hasOfferDetailsLoadingError = false; + notifyListeners(); + } + + /// Format date to "31 Aug 2026" format + String formatExpiryDate(DateTime date) { + final List months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + + final day = date.day; + final month = months[date.month - 1]; + final year = date.year; + + return '$day $month $year'; + } + /// Authenticate patient before making a purchase Future authenticatePatient({Function(PomsAuthTokenModel)? onSuccess, Function(String)? onError}) async { final appState = getIt.get(); @@ -512,6 +539,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { stopAutoPlayTimer(); _progress = 0.0; _currentPage = 0; + hasOfferDetailsLoadingError = false; isOfferDetailsLoading = true; notifyListeners(); @@ -521,6 +549,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { result.fold( (failure) async { isOfferDetailsLoading = false; + hasOfferDetailsLoadingError = true; notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { @@ -529,6 +558,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { }, (response) { isOfferDetailsLoading = false; + hasOfferDetailsLoadingError = false; if (response.data != null) { offerDetails = response.data; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 29b0c173..7b1547db 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -231,7 +231,7 @@ class _LandingPageState extends State { immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); offersAndDiscountsViewModel.setIsoffersListLoading(true); - offersAndDiscountsViewModel.initOffersAndDiscounts(); + offersAndDiscountsViewModel.initOffersAndDiscounts(forceRefresh: true); // appointmentRatingViewModel.getLastRatingAppointment( // onSuccess: (response) { @@ -555,7 +555,7 @@ class _LandingPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), + (LocaleKeys.offerAndPackages.tr(context: context)).toText16(isBold: true), Row( children: [ LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), diff --git a/lib/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart b/lib/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart index 50e3a27a..57afaba7 100644 --- a/lib/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart +++ b/lib/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart @@ -39,6 +39,16 @@ class _OffersAndDiscountsSuccessQrPageState extends State? _qrCodeFuture; // Cache the QR code future + + @override + void initState() { + super.initState(); + // Initialize the QR code future once + if (widget.purchaseData.qrCode != null && widget.purchaseData.qrCode!.isNotEmpty) { + _qrCodeFuture = Utils.createFileFromString(widget.purchaseData.qrCode!, "png"); + } + } Color _getStatusColor(String status) { switch (status) { @@ -92,8 +102,10 @@ class _OffersAndDiscountsSuccessQrPageState extends State(); - print(widget.isFromPaymentScreen); return Consumer( builder: (context, viewModel, child) { offersAndDiscountsViewModel = viewModel; - return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -171,7 +192,7 @@ class _OffersAndDiscountsSuccessQrPageState extends State( - future: Utils.createFileFromString(widget.purchaseData.qrCode!, "png"), + future: _qrCodeFuture, // Use cached future builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center( diff --git a/lib/presentation/offers_and_discounts/offers_and_discount_payment_page.dart b/lib/presentation/offers_and_discounts/offers_and_discount_payment_page.dart index c0b6a5f8..03b227a3 100644 --- a/lib/presentation/offers_and_discounts/offers_and_discount_payment_page.dart +++ b/lib/presentation/offers_and_discounts/offers_and_discount_payment_page.dart @@ -76,9 +76,7 @@ class _OffersAndDiscountPaymentPageState extends State item.offer!.id == selectedOffer.id, - orElse: () => offersAndDiscountsViewModel.historyList.first, - ); - + final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere((item) => item.offer!.id == selectedOffer.id, orElse: () => offersAndDiscountsViewModel.historyList.first); + LoaderBottomSheet.hideLoader(); // Navigate to success QR page with purchase data Navigator.of(context).pushReplacement( CustomPageRoute( - page: OffersAndDiscountsSuccessQrPage(purchaseData: purchaseData, isFromPaymentScreen: true,), + page: OffersAndDiscountsSuccessQrPage( + purchaseData: purchaseData, + isFromPaymentScreen: true, + ), ), ); }, 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 ec302429..3d51df67 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 @@ -7,6 +7,7 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -37,6 +38,7 @@ class _OffersAndDiscountsDetailedPageState extends State(context, listen: false); // Clear previous offer details to prevent showing old data viewModel.offerDetails = null; + // Reset image loaded state + setState(() { + isImageLoaded = false; + }); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); viewModel.getOfferDetails( @@ -73,14 +79,6 @@ class _OffersAndDiscountsDetailedPageState extends State(context, listen: false); return CollapsingListView( - title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}", + title: "${LocaleKeys.offerAndPackageDetails.tr(context: context)}", bottomChild: CustomButton( text: LocaleKeys.buyNow.tr(context: context), icon: AppAssets.shoppingCart, @@ -179,9 +177,37 @@ class _OffersAndDiscountsDetailedPageState extends State 0) - "${line.discountValue}% ${LocaleKeys.off.tr(context: context)}".toText14( - color: AppColors.primaryRedColor, - isBold: true, - ), + "${LocaleKeys.quantity.tr(context: context)}: ${line.quantity}".toText14(color: AppColors.greyTextColor), + if (line.discountValue > 0) "${line.discountValue}% ${LocaleKeys.off.tr(context: context)}".toText14(color: AppColors.primaryRedColor, isBold: true), ], ), ], @@ -404,4 +422,20 @@ class _OffersAndDiscountsDetailedPageState extends State(); return CollapsingListView( - title: LocaleKeys.order.tr(context: context), + title: LocaleKeys.myOrders.tr(context: context), isLeading: true, child: SingleChildScrollView( child: Consumer(builder: (context, offersAndDiscountVM, child) { @@ -84,7 +85,7 @@ class _OffersAndDiscountsHistoryPageState extends State chips = history.branches!.map((branch) { final hospitalName = isArabic ? branch.nameAr : branch.name; return AppCustomChipWidget( - labelText: "Hospital: $hospitalName", + labelText: "${LocaleKeys.hospital.tr()}: $hospitalName", backgroundColor: AppColors.chipBgColor, textColor: AppColors.textColor, icon: AppAssets.hospital, @@ -283,7 +310,7 @@ class _OffersAndDiscountsHistoryPageState extends State { // Authenticate patient once when visiting the offers page if (!appState.isPomsAuthTokenValid()) { + // Show loader during authentication + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + await offersAndDiscountsViewModel.authenticatePatient( onSuccess: (authToken) { + // Hide loader on success + LoaderBottomSheet.hideLoader(); print("POMS Authentication successful: ${authToken.token}"); }, onError: (error) { + // Hide loader on error + LoaderBottomSheet.hideLoader(); print("POMS Authentication failed: $error"); // Authentication failed, but user can still browse offers }, @@ -156,12 +164,10 @@ class _OffersAndDiscountsPageState extends State { offersAndDiscountsViewModel = Provider.of(context, listen: false); return CollapsingListView( - title: "${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}", + title: LocaleKeys.offerAndPackages.tr(context: context), history: () { Navigator.of(context).push( - CustomPageRoute( - page: OffersAndDiscountsHistoryPage(), - ), + CustomPageRoute(page: OffersAndDiscountsHistoryPage()), ); }, child: SingleChildScrollView( @@ -305,7 +311,23 @@ class _OffersAndDiscountsPageState extends State { SizedBox(height: 8.h), // Valid till chip AppCustomChipWidget( - labelText: "${LocaleKeys.validTill.tr(context: context)} ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].validTo.toString())}", + // labelText: "${LocaleKeys.validTill.tr(context: context)}: ${DateUtil.formatDateToDate(offersAndDiscountVM.filteredOffers[index].validTo!, appState.getLanguageCode() == "ar" ? true : false)}", + + richText: Row( + mainAxisSize: MainAxisSize.min, + children: [ + "${LocaleKeys.validTill.tr(context: context)}: ".toText10(color: AppColors.textColor, isBold: true, weight: FontWeight.w700), + Directionality( + textDirection: ui.TextDirection.ltr, + child: offersAndDiscountVM.formatExpiryDate(offersAndDiscountVM.filteredOffers[index].validTo!).toText10( + isEnglishOnly: true, + isBold: true, + weight: FontWeight.w700, + color: AppColors.textColor, + )), + ], + ), + backgroundColor: AppColors.chipBgColor, textColor: AppColors.textColor, icon: AppAssets.calendar,