Offers And Discounts Complete

offers_discounts
Aamir Muhammad 1 month ago
parent d4d245d299
commit ea124325f0

@ -25,6 +25,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isAuthenticating = false; bool isAuthenticating = false;
bool isPurchasing = false; bool isPurchasing = false;
bool isSaveQrCodePressed = false; // Track QR code capture state bool isSaveQrCodePressed = false; // Track QR code capture state
bool hasOfferDetailsLoadingError = false; // Track offer details loading error
List<int> selectedCategories = []; // Empty means 'All Offers' List<int> selectedCategories = []; // Empty means 'All Offers'
String searchQuery = ''; String searchQuery = '';
@ -235,6 +236,32 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners(); 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<String> 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 /// Authenticate patient before making a purchase
Future<bool> authenticatePatient({Function(PomsAuthTokenModel)? onSuccess, Function(String)? onError}) async { Future<bool> authenticatePatient({Function(PomsAuthTokenModel)? onSuccess, Function(String)? onError}) async {
final appState = getIt.get<AppState>(); final appState = getIt.get<AppState>();
@ -512,6 +539,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
stopAutoPlayTimer(); stopAutoPlayTimer();
_progress = 0.0; _progress = 0.0;
_currentPage = 0; _currentPage = 0;
hasOfferDetailsLoadingError = false;
isOfferDetailsLoading = true; isOfferDetailsLoading = true;
notifyListeners(); notifyListeners();
@ -521,6 +549,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
result.fold( result.fold(
(failure) async { (failure) async {
isOfferDetailsLoading = false; isOfferDetailsLoading = false;
hasOfferDetailsLoadingError = true;
notifyListeners(); notifyListeners();
await errorHandlerService.handleError(failure: failure); await errorHandlerService.handleError(failure: failure);
if (onError != null) { if (onError != null) {
@ -529,6 +558,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}, },
(response) { (response) {
isOfferDetailsLoading = false; isOfferDetailsLoading = false;
hasOfferDetailsLoadingError = false;
if (response.data != null) { if (response.data != null) {
offerDetails = response.data; offerDetails = response.data;
} }

@ -231,7 +231,7 @@ class _LandingPageState extends State<LandingPage> {
immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory(); immediateLiveCareViewModel.getPatientLiveCareHistory();
offersAndDiscountsViewModel.setIsoffersListLoading(true); offersAndDiscountsViewModel.setIsoffersListLoading(true);
offersAndDiscountsViewModel.initOffersAndDiscounts(); offersAndDiscountsViewModel.initOffersAndDiscounts(forceRefresh: true);
// appointmentRatingViewModel.getLastRatingAppointment( // appointmentRatingViewModel.getLastRatingAppointment(
// onSuccess: (response) { // onSuccess: (response) {
@ -555,7 +555,7 @@ class _LandingPageState extends State<LandingPage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), (LocaleKeys.offerAndPackages.tr(context: context)).toText16(isBold: true),
Row( Row(
children: [ children: [
LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),

@ -39,6 +39,16 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
late OffersAndDiscountsViewModel offersAndDiscountsViewModel; late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
String? qrCodeFilePath; // Store the QR code file path for sharing String? qrCodeFilePath; // Store the QR code file path for sharing
final GlobalKey _screenshotKey = GlobalKey(); final GlobalKey _screenshotKey = GlobalKey();
Future<String>? _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) { Color _getStatusColor(String status) {
switch (status) { switch (status) {
@ -92,8 +102,10 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
final result = await offersAndDiscountsViewModel.handleQRCodeAction( final result = await offersAndDiscountsViewModel.handleQRCodeAction(
screenshotKey: _screenshotKey, screenshotKey: _screenshotKey,
voucherCode: widget.purchaseData.voucherCode!, voucherCode: widget.purchaseData.voucherCode!,
shouldSaveToStorage: true, // Always save to permanent storage shouldSaveToStorage: true,
isFromPaymentScreen: isShareAction ? false : widget.isFromPaymentScreen, // Share action never pops // Always save to permanent storage
isFromPaymentScreen: widget.isFromPaymentScreen,
// Always use the actual value
context: context, context: context,
offerName: widget.purchaseData.offer!.name, offerName: widget.purchaseData.offer!.name,
offerNameAr: widget.purchaseData.offer!.nameAr, offerNameAr: widget.purchaseData.offer!.nameAr,
@ -103,18 +115,29 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
if (result['success']) { if (result['success']) {
if (isShareAction) { if (isShareAction) {
// Share action: Saved to permanent storage, now share the file // Share button pressed: Open share dialog
final xFile = XFile(result['filePath']); final xFile = XFile(result['filePath']);
final offerName = result['offerName']; final offerName = result['offerName'];
// Share the file
// ignore: deprecated_member_use // ignore: deprecated_member_use
await Share.shareXFiles( await Share.shareXFiles(
[xFile], [xFile],
text: '${LocaleKeys.saveQRCode.tr(context: context)} - ${LocaleKeys.orderLabel.tr(context: context)} #${widget.purchaseData.voucherCode} - $offerName', text: '${LocaleKeys.saveQRCode.tr(context: context)} - ${LocaleKeys.orderLabel.tr(context: context)} #${widget.purchaseData.voucherCode} - $offerName',
subject: LocaleKeys.orderDetails.tr(context: context), subject: LocaleKeys.orderDetails.tr(context: context),
); );
// If from payment screen, navigate after share (whether user shared or cancelled)
if (widget.isFromPaymentScreen) {
if (mounted) {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsHistoryPage()));
}
}
} else { } else {
// Save action: Show success and optionally navigate // Save QR Code button pressed: Show success and handle navigation based on source
if (result['isFromPayment']) { if (widget.isFromPaymentScreen) {
// From payment screen: Show success bottom sheet and navigate to history // From payment screen: Show success bottom sheet and navigate to history
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
@ -156,11 +179,9 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
print(widget.isFromPaymentScreen);
return Consumer<OffersAndDiscountsViewModel>( return Consumer<OffersAndDiscountsViewModel>(
builder: (context, viewModel, child) { builder: (context, viewModel, child) {
offersAndDiscountsViewModel = viewModel; offersAndDiscountsViewModel = viewModel;
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView( body: CollapsingListView(
@ -171,7 +192,7 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
bottomChild: CustomButton( bottomChild: CustomButton(
text: LocaleKeys.saveQRCode.tr(context: context), text: LocaleKeys.saveQRCode.tr(context: context),
onPressed: () { onPressed: () {
_handleQRCodeAction(isShareAction: false); // Save action - saves and shows success _handleQRCodeAction(isShareAction: true); // Save action - saves and shows success
}, },
backgroundColor: AppColors.successColor, backgroundColor: AppColors.successColor,
borderColor: AppColors.successColor, borderColor: AppColors.successColor,
@ -199,7 +220,17 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
SizedBox(height: 6.h), SizedBox(height: 6.h),
_buildQRCode().paddingSymmetrical(0.h, 0.h), _buildQRCode().paddingSymmetrical(0.h, 0.h),
AppCustomChipWidget( AppCustomChipWidget(
labelText: "${LocaleKeys.expiresOn.tr(context: context)}: ${Utils.formatDateToDisplay(widget.purchaseData.offer!.validTo.toString())}", richText: Row(
mainAxisSize: MainAxisSize.min,
children: [
"${LocaleKeys.expiresOn.tr(context: context)}: ".toText10(isEnglishOnly: true, color: AppColors.primaryRedColor, isBold: true, weight: FontWeight.w700),
Directionality(
textDirection: ui.TextDirection.ltr,
child: viewModel
.formatExpiryDate(widget.purchaseData.offer!.validTo!)
.toText10(isEnglishOnly: true, isBold: true, weight: FontWeight.w700, color: AppColors.primaryRedColor)),
],
),
backgroundColor: AppColors.lightRedButtonColor, backgroundColor: AppColors.lightRedButtonColor,
textColor: AppColors.primaryRedColor, textColor: AppColors.primaryRedColor,
icon: "", icon: "",
@ -288,7 +319,7 @@ class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSucc
} }
return FutureBuilder<String>( return FutureBuilder<String>(
future: Utils.createFileFromString(widget.purchaseData.qrCode!, "png"), future: _qrCodeFuture, // Use cached future
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return Center( return Center(

@ -76,9 +76,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
// If Saudi user (patientType == 1), return price without VAT // If Saudi user (patientType == 1), return price without VAT
// Otherwise, return full price with VAT // Otherwise, return full price with VAT
return authenticatedUser.patientType == 1 return authenticatedUser.patientType == 1 ? selectedOffer.vatExclusivePrice : selectedOffer.listPrice;
? selectedOffer.vatExclusivePrice
: selectedOffer.listPrice;
} }
@override @override
@ -111,7 +109,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
children: [ children: [
Expanded( Expanded(
child: CollapsingListView( child: CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.payment.tr(context: context)}", title: "${LocaleKeys.offerAndPackages.tr(context: context)} ${LocaleKeys.payment.tr(context: context)}",
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -362,9 +360,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
LocaleKeys.vat15.tr().toText14(isBold: true, color: AppColors.greyTextColor), LocaleKeys.vat15.tr().toText14(isBold: true, color: AppColors.greyTextColor),
// Check if user is Saudi (patientType == 1 means Saudi) // Check if user is Saudi (patientType == 1 means Saudi)
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
(appState.getAuthenticatedUser()?.patientType == 1 (appState.getAuthenticatedUser()?.patientType == 1 ? "0.00" : (selectedOffer.vatInclusivePrice - selectedOffer.vatExclusivePrice).toStringAsFixed(2))
? "0.00"
: (selectedOffer.vatInclusivePrice - selectedOffer.vatExclusivePrice).toStringAsFixed(2))
.toText14(isBold: true, color: AppColors.greyTextColor), .toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor, AppColors.greyTextColor,
13, 13,
@ -378,10 +374,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
"".toText14(isBold: true), "".toText14(isBold: true),
// If Saudi user (patientType == 1), show price without VAT, otherwise show full price with VAT // If Saudi user (patientType == 1), show price without VAT, otherwise show full price with VAT
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
(appState.getAuthenticatedUser()?.patientType == 1 (appState.getAuthenticatedUser()?.patientType == 1 ? selectedOffer.vatExclusivePrice.toString() : selectedOffer.listPrice.toString()).toText24(isBold: true),
? selectedOffer.vatExclusivePrice.toString()
: selectedOffer.listPrice.toString())
.toText24(isBold: true),
AppColors.blackColor, AppColors.blackColor,
17, 17,
isSaudiCurrency: selectedOffer.currency == "SAR"), isSaudiCurrency: selectedOffer.currency == "SAR"),
@ -744,18 +737,16 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
// Payment result sent successfully, now fetch purchase history // Payment result sent successfully, now fetch purchase history
await offersAndDiscountsViewModel.getOffersAndDiscountsHistory( await offersAndDiscountsViewModel.getOffersAndDiscountsHistory(
onSuccess: (historyResponse) { onSuccess: (historyResponse) {
LoaderBottomSheet.hideLoader();
// Find the purchased offer in the history by matching the offer ID // Find the purchased offer in the history by matching the offer ID
final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere( final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere((item) => item.offer!.id == selectedOffer.id, orElse: () => offersAndDiscountsViewModel.historyList.first);
(item) => item.offer!.id == selectedOffer.id, LoaderBottomSheet.hideLoader();
orElse: () => offersAndDiscountsViewModel.historyList.first,
);
// Navigate to success QR page with purchase data // Navigate to success QR page with purchase data
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
CustomPageRoute( CustomPageRoute(
page: OffersAndDiscountsSuccessQrPage(purchaseData: purchaseData, isFromPaymentScreen: true,), page: OffersAndDiscountsSuccessQrPage(
purchaseData: purchaseData,
isFromPaymentScreen: true,
),
), ),
); );
}, },

@ -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_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.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/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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -37,6 +38,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
late OffersAndDiscountsViewModel viewModel; late OffersAndDiscountsViewModel viewModel;
late AppState appState; late AppState appState;
bool hasLoadingError = false; bool hasLoadingError = false;
bool isImageLoaded = false;
@override @override
void initState() { void initState() {
@ -45,6 +47,10 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false); viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
// Clear previous offer details to prevent showing old data // Clear previous offer details to prevent showing old data
viewModel.offerDetails = null; viewModel.offerDetails = null;
// Reset image loaded state
setState(() {
isImageLoaded = false;
});
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
viewModel.getOfferDetails( viewModel.getOfferDetails(
@ -73,14 +79,6 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
super.dispose(); super.dispose();
} }
String _formatDateString(DateTime date) {
try {
return DateFormat('d MMM, yyyy').format(date);
} catch (e) {
return '';
}
}
// Determine the status of the offer based on dates // Determine the status of the offer based on dates
String _getOfferStatus(DateTime validTo) { String _getOfferStatus(DateTime validTo) {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
@ -119,7 +117,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false); viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView( return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}", title: "${LocaleKeys.offerAndPackageDetails.tr(context: context)}",
bottomChild: CustomButton( bottomChild: CustomButton(
text: LocaleKeys.buyNow.tr(context: context), text: LocaleKeys.buyNow.tr(context: context),
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
@ -179,9 +177,37 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
); );
} }
// Only show offer if it matches the requested offerId (prevents showing old cached data) // Show skeleton loader while offer data is being loaded
if (offer == null || offer.id != widget.offerId) { if (offer == null || offer.id != widget.offerId) {
return SizedBox.shrink(); // Return empty widget while loading return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Skeleton loader for image
_buildImageLoadingShimmer(),
SizedBox(height: 24.h),
// Skeleton loader for title
_buildTextShimmer(width: 250.w, height: 24.h),
SizedBox(height: 16.h),
// Skeleton loader for price
_buildTextShimmer(width: 150.w, height: 18.h),
SizedBox(height: 16.h),
// Skeleton loaders for chips
Row(
children: [
_buildTextShimmer(width: 120.w, height: 32.h),
SizedBox(width: 12.w),
_buildTextShimmer(width: 80.w, height: 32.h),
SizedBox(width: 12.w),
_buildTextShimmer(width: 100.w, height: 32.h),
],
),
SizedBox(height: 24.h),
// Skeleton loader for description
_buildTextShimmer(width: double.infinity, height: 100.h),
],
).paddingSymmetrical(24.h, 0.h),
);
} }
final status = _getOfferStatus(offer.validTo); final status = _getOfferStatus(offer.validTo);
@ -199,7 +225,10 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
height: 250.h, height: 250.h,
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) {
return child;
}
// Show shimmer while image is loading
return _buildImageLoadingShimmer(); return _buildImageLoadingShimmer();
}, },
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
@ -225,7 +254,10 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
height: 250.h, height: 250.h,
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) {
return child;
}
// Show shimmer while image is loading
return _buildImageLoadingShimmer(); return _buildImageLoadingShimmer();
}, },
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
@ -255,10 +287,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr, textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [ children: [
"${LocaleKeys.price.tr(context: context)}: ".toText18(isBold: true, weight: FontWeight.w700), "${LocaleKeys.price.tr(context: context)}: ".toText18(isBold: true, weight: FontWeight.w700),
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(((offer.listPrice).toStringAsFixed(2)).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13,
((offer.listPrice).toStringAsFixed(2)).toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: offer.currency == "SAR") isSaudiCurrency: offer.currency == "SAR")
], ],
), ),
@ -273,7 +302,22 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
children: [ children: [
// Valid till chip // Valid till chip
AppCustomChipWidget( AppCustomChipWidget(
labelText: "${LocaleKeys.validTill.tr(context: context)} ${_formatDateString(offer.validTo)}", // labelText: "${LocaleKeys.validTill.tr(context: context)} ${DateUtil.formatDateToDate(offer.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: viewModel.formatExpiryDate(offer.validTo).toText10(
isEnglishOnly: true,
isBold: true,
weight: FontWeight.w700,
color: AppColors.textColor,
)),
],
),
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.calendar, icon: AppAssets.calendar,
@ -319,29 +363,12 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
], ],
// Description Heading
LocaleKeys.description.tr(context: context).toText18(isBold: true, weight: FontWeight.w700), LocaleKeys.description.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
SizedBox(height: 12.h), SizedBox(height: 12.h),
HtmlWidget((appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description)),
// Description Content
HtmlWidget(
(appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description),
),
//.toText16(
// color: AppColors.greyTextColor,
// height: 1.5,
// )
SizedBox(height: 24.h), SizedBox(height: 24.h),
// Offer Lines Section
if (offer.offerLines.isNotEmpty) ...[ if (offer.offerLines.isNotEmpty) ...[
LocaleKeys.includedServices.tr(context: context).toText18( LocaleKeys.includedServices.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
isBold: true,
weight: FontWeight.w700,
),
SizedBox(height: 12.h), SizedBox(height: 12.h),
...offer.offerLines.map((line) { ...offer.offerLines.map((line) {
return Container( return Container(
@ -355,23 +382,14 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
(appState.getLanguageID() == 1 ? line.nameAr : line.name).toText16( (appState.getLanguageID() == 1 ? line.nameAr : line.name).toText16(isBold: true, weight: FontWeight.w600),
isBold: true,
weight: FontWeight.w600,
),
SizedBox(height: 4.h), SizedBox(height: 4.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr, textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [ children: [
"${LocaleKeys.quantity.tr(context: context)}: ${line.quantity}".toText14( "${LocaleKeys.quantity.tr(context: context)}: ${line.quantity}".toText14(color: AppColors.greyTextColor),
color: AppColors.greyTextColor, if (line.discountValue > 0) "${line.discountValue}% ${LocaleKeys.off.tr(context: context)}".toText14(color: AppColors.primaryRedColor, isBold: true),
),
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<OffersAndDiscountsDetai
), ),
); );
} }
// Helper method to build shimmer loading placeholder for text elements
Widget _buildTextShimmer({required double width, required double height}) {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8.h),
),
),
);
}
} }

@ -1,11 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.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/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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -51,7 +52,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
return CollapsingListView( return CollapsingListView(
title: LocaleKeys.order.tr(context: context), title: LocaleKeys.myOrders.tr(context: context),
isLeading: true, isLeading: true,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) { child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
@ -84,7 +85,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
child: SlideAnimation( child: SlideAnimation(
verticalOffset: 100.0, verticalOffset: 100.0,
child: FadeInAnimation( child: FadeInAnimation(
child: _buildHistoryCard(history), child: _buildHistoryCard(history, offersAndDiscountVM),
), ),
), ),
); );
@ -122,7 +123,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
); );
} }
Widget _buildHistoryCard(Datum history) { Widget _buildHistoryCard(Datum history, OffersAndDiscountsViewModel viewModel) {
return AnimatedContainer( return AnimatedContainer(
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -151,27 +152,53 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
runSpacing: 8.h, runSpacing: 8.h,
children: [ children: [
AppCustomChipWidget( AppCustomChipWidget(
labelText: "${LocaleKeys.orderLabel.tr(context: context)}: ${history.voucherCode}", labelText: "${LocaleKeys.voucherNo.tr(context: context)}: ${history.voucherCode}",
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.my_sick_leave_icon, icon: AppAssets.my_sick_leave_icon,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w), labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
), ),
AppCustomChipWidget( AppCustomChipWidget(
labelText: "${LocaleKeys.purchased.tr(context: context)}: ${_formatDateChipFromDateTime(history.purchaseDate!)}", richText: Row(
mainAxisSize: MainAxisSize.min,
children: [
"${LocaleKeys.purchased.tr(context: context)}: ".toText10(color: AppColors.textColor, isBold: true, weight: FontWeight.w700),
Directionality(
textDirection: ui.TextDirection.ltr,
child: viewModel.formatExpiryDate(history.purchaseDate!).toText10(
isEnglishOnly: true,
isBold: true,
weight: FontWeight.w700,
color: AppColors.textColor,
)),
],
),
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.calendar, icon: AppAssets.calendar,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w), labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
isEnglishOnly: true,
), ),
history.offer!.validTo != null history.offer!.validTo != null
? AppCustomChipWidget( ? AppCustomChipWidget(
labelText: "${LocaleKeys.validUntil.tr(context: context)}: ${_formatDateChipFromDateTime(history.offer!.validTo!)}", richText: Row(
mainAxisSize: MainAxisSize.min,
children: [
"${LocaleKeys.validUntil.tr(context: context)}: ".toText10(color: AppColors.textColor, isBold: true, weight: FontWeight.w700),
Directionality(
textDirection: ui.TextDirection.ltr,
child: viewModel.formatExpiryDate(history.offer!.validTo!).toText10(
isEnglishOnly: true,
isBold: true,
weight: FontWeight.w700,
color: AppColors.textColor,
)),
],
),
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.calendar, icon: AppAssets.calendar,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w), labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
isEnglishOnly: true,
) )
: SizedBox(), : SizedBox(),
// Hospital chips - show multiple chips when expanded // Hospital chips - show multiple chips when expanded
@ -243,7 +270,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
if (history.branches == null || history.branches!.isEmpty) { if (history.branches == null || history.branches!.isEmpty) {
return [ return [
AppCustomChipWidget( AppCustomChipWidget(
labelText: "Hospital: N/A", labelText: "${LocaleKeys.hospital.tr()}: N/A",
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.hospital, icon: AppAssets.hospital,
@ -257,7 +284,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
final hospitalName = isArabic ? history.branches!.first.nameAr : history.branches!.first.name; final hospitalName = isArabic ? history.branches!.first.nameAr : history.branches!.first.name;
return [ return [
AppCustomChipWidget( AppCustomChipWidget(
labelText: "Hospital: $hospitalName", labelText: "${LocaleKeys.hospital.tr()}: $hospitalName",
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.hospital, icon: AppAssets.hospital,
@ -272,7 +299,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
List<Widget> chips = history.branches!.map<Widget>((branch) { List<Widget> chips = history.branches!.map<Widget>((branch) {
final hospitalName = isArabic ? branch.nameAr : branch.name; final hospitalName = isArabic ? branch.nameAr : branch.name;
return AppCustomChipWidget( return AppCustomChipWidget(
labelText: "Hospital: $hospitalName", labelText: "${LocaleKeys.hospital.tr()}: $hospitalName",
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.hospital, icon: AppAssets.hospital,
@ -283,7 +310,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
// Add a "Show less" chip at the end // Add a "Show less" chip at the end
chips.add( chips.add(
AppCustomChipWidget( AppCustomChipWidget(
labelText: "Show less", labelText: LocaleKeys.showLess.tr(),
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
textColor: AppColors.primaryRedColor, textColor: AppColors.primaryRedColor,
icon: "", icon: "",
@ -303,7 +330,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
final remainingCount = history.branches!.length - 1; final remainingCount = history.branches!.length - 1;
return [ return [
AppCustomChipWidget( AppCustomChipWidget(
labelText: "Hospital: $firstHospital (+$remainingCount more)", labelText: "${LocaleKeys.hospital.tr()}: $firstHospital (+$remainingCount more)",
backgroundColor: AppColors.chipBgColor, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.hospital, icon: AppAssets.hospital,

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -8,8 +7,10 @@ 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/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.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/enums.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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_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/generated/locale_keys.g.dart';
@ -50,11 +51,18 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
// Authenticate patient once when visiting the offers page // Authenticate patient once when visiting the offers page
if (!appState.isPomsAuthTokenValid()) { if (!appState.isPomsAuthTokenValid()) {
// Show loader during authentication
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
await offersAndDiscountsViewModel.authenticatePatient( await offersAndDiscountsViewModel.authenticatePatient(
onSuccess: (authToken) { onSuccess: (authToken) {
// Hide loader on success
LoaderBottomSheet.hideLoader();
print("POMS Authentication successful: ${authToken.token}"); print("POMS Authentication successful: ${authToken.token}");
}, },
onError: (error) { onError: (error) {
// Hide loader on error
LoaderBottomSheet.hideLoader();
print("POMS Authentication failed: $error"); print("POMS Authentication failed: $error");
// Authentication failed, but user can still browse offers // Authentication failed, but user can still browse offers
}, },
@ -156,12 +164,10 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false); offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView( return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}", title: LocaleKeys.offerAndPackages.tr(context: context),
history: () { history: () {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(page: OffersAndDiscountsHistoryPage()),
page: OffersAndDiscountsHistoryPage(),
),
); );
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
@ -305,7 +311,23 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
SizedBox(height: 8.h), SizedBox(height: 8.h),
// Valid till chip // Valid till chip
AppCustomChipWidget( 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, backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,
icon: AppAssets.calendar, icon: AppAssets.calendar,

Loading…
Cancel
Save