Offers And Discounts Complete

offers_discounts
Aamir Muhammad 1 month ago
parent b02831834f
commit d4d245d299

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.9384 2C5.28428 1.99994 4.69656 1.99989 4.22085 2.06384C3.70149 2.13367 3.16868 2.2958 2.73224 2.73224C2.2958 3.16868 2.13367 3.70149 2.06384 4.22085C1.99989 4.69656 1.99994 5.28428 2 5.9384V6.06161C1.99994 6.71574 1.99989 7.30346 2.06384 7.77916C2.13367 8.29852 2.2958 8.83134 2.73224 9.26777C3.16868 9.70421 3.70149 9.86635 4.22085 9.93617C4.69656 10.0001 5.28427 10.0001 5.9384 10H6.06162C6.71574 10.0001 7.30346 10.0001 7.77916 9.93617C8.29852 9.86635 8.83134 9.70421 9.26777 9.26777C9.70421 8.83134 9.86635 8.29852 9.93617 7.77916C10.0001 7.30346 10.0001 6.71574 10 6.06162V5.9384C10.0001 5.28427 10.0001 4.69656 9.93617 4.22085C9.86635 3.70149 9.70421 3.16868 9.26777 2.73224C8.83134 2.2958 8.29852 2.13367 7.77916 2.06384C7.30346 1.99989 6.71574 1.99994 6.06161 2H5.9384Z" fill="white"/>
<path d="M5.9384 14C5.28428 13.9999 4.69655 13.9999 4.22085 14.0638C3.70149 14.1337 3.16868 14.2958 2.73224 14.7322C2.2958 15.1687 2.13367 15.7015 2.06384 16.2209C1.99989 16.6966 1.99994 17.2843 2 17.9384V18.0616C1.99994 18.7157 1.99989 19.3035 2.06384 19.7792C2.13367 20.2985 2.2958 20.8313 2.73224 21.2678C3.16868 21.7042 3.70149 21.8663 4.22085 21.9362C4.69656 22.0001 5.28427 22.0001 5.9384 22H6.06162C6.71574 22.0001 7.30346 22.0001 7.77916 21.9362C8.29852 21.8663 8.83134 21.7042 9.26777 21.2678C9.70421 20.8313 9.86635 20.2985 9.93617 19.7792C10.0001 19.3035 10.0001 18.7157 10 18.0616V17.9384C10.0001 17.2843 10.0001 16.6966 9.93617 16.2209C9.86635 15.7015 9.70421 15.1687 9.26777 14.7322C8.83134 14.2958 8.29852 14.1337 7.77916 14.0638C7.30346 13.9999 6.71575 13.9999 6.06163 14H5.9384Z" fill="white"/>
<path d="M2 12C2 11.4477 2.44772 11 3 11L9 11C9.55228 11 10 11.4477 10 12C10 12.5523 9.55228 13 9 13L3 13C2.44772 13 2 12.5523 2 12Z" fill="white"/>
<path d="M13 3C13 2.44772 12.5523 2 12 2C11.4477 2 11 2.44772 11 3L11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8L13 3Z" fill="white"/>
<path d="M17.9384 2H18.0616C18.7157 1.99994 19.3035 1.99989 19.7792 2.06384C20.2985 2.13367 20.8313 2.2958 21.2678 2.73224C21.7042 3.16868 21.8663 3.70149 21.9362 4.22085C22.0001 4.69656 22.0001 5.28427 22 5.9384V6.06162C22.0001 6.71574 22.0001 7.30346 21.9362 7.77916C21.8663 8.29852 21.7042 8.83134 21.2678 9.26777C20.8313 9.70421 20.2985 9.86635 19.7792 9.93617C19.3035 10.0001 18.7157 10.0001 18.0616 10H17.9384C17.2843 10.0001 16.6966 10.0001 16.2209 9.93617C15.7015 9.86635 15.1687 9.70421 14.7322 9.26777C14.2958 8.83134 14.1337 8.29852 14.0638 7.77916C13.9999 7.30346 13.9999 6.71575 14 6.06163V5.9384C13.9999 5.28428 13.9999 4.69655 14.0638 4.22085C14.1337 3.70149 14.2958 3.16868 14.7322 2.73224C15.1687 2.2958 15.7015 2.13367 16.2209 2.06384C16.6966 1.99989 17.2843 1.99994 17.9384 2Z" fill="white"/>
<path d="M14.9384 11C14.2843 10.9999 13.6966 10.9999 13.2209 11.0638C12.7015 11.1337 12.1687 11.2958 11.7322 11.7322C11.2958 12.1687 11.1337 12.7015 11.0638 13.2209C10.9999 13.6966 10.9999 14.2843 11 14.9384L11 15C11 15.5523 11.4477 16 12 16C12.5523 16 13 15.5523 13 15C13 14.2646 13.0021 13.8137 13.046 13.4873C13.0846 13.2005 13.1394 13.1526 13.1459 13.1469L13.1469 13.1459C13.1526 13.1394 13.2005 13.0846 13.4873 13.046C13.8137 13.0021 14.2646 13 15 13L21 13C21.5523 13 22 12.5523 22 12C22 11.4477 21.5523 11 21 11L14.9384 11Z" fill="white"/>
<path d="M18 16C18.7354 16 19.1862 16.0021 19.5125 16.0461C19.799 16.0846 19.8468 16.1395 19.8526 16.146L19.8536 16.1471C19.8604 16.1531 19.9154 16.2016 19.954 16.4891C19.9979 16.8162 20 17.268 20 18.0043C20 18.7407 19.9979 19.1925 19.954 19.5196C19.9154 19.8071 19.8604 19.8556 19.8536 19.8616L19.8522 19.863C19.8442 19.8712 19.8181 19.8979 19.7034 19.9272C19.5612 19.9636 19.3416 19.9892 18.9698 20.0005C18.4178 20.0171 17.9838 20.4781 18.0005 21.0302C18.0171 21.5822 18.4781 22.0162 19.0302 21.9996C19.7921 21.9766 20.64 21.9045 21.2683 21.2753C21.7045 20.8385 21.8664 20.3054 21.9362 19.7857C22.0001 19.3095 22.0001 18.7211 22 18.066V17.9428C22.0001 17.2876 22.0001 16.6992 21.9362 16.223C21.8664 15.7033 21.7045 15.1702 21.2683 14.7334C20.8319 14.2964 20.299 14.1339 19.7794 14.0639C19.3036 13.9999 18.7157 13.9999 18.0617 14L18 14C17.4477 14 17 14.4477 17 15C17 15.5523 17.4477 16 18 16Z" fill="white"/>
<path d="M16 15C16 14.4477 15.5523 14 15 14C14.4477 14 14 14.4477 14 15V16.5C14 17.394 14.2542 18.2178 15.0565 18.6656C15.3946 18.8543 15.7549 18.928 16.0621 18.9627C16.3698 18.9975 16.6995 19 17 19C17 19.5523 17.4477 20 18 20C18.5523 20 19 19.5523 19 19C19 17.8954 18.1046 17 17 17C16.6924 17 16.468 16.9958 16.2866 16.9753C16.152 16.9601 16.0822 16.9397 16.051 16.9279C16.0333 16.8814 16 16.7576 16 16.5V15Z" fill="white"/>
<path d="M13 17.7692C13 17.217 12.5523 16.7692 12 16.7692C11.4477 16.7692 11 17.217 11 17.7692L11 20.5385C11 21.0908 11.4477 21.5385 12 21.5385C12.5523 21.5385 13 21.0908 13 20.5385L13 17.7692Z" fill="white"/>
<path d="M15 20C14.4477 20 14 20.4477 14 21C14 21.5523 14.4477 22 15 22H16C16.5523 22 17 21.5523 17 21C17 20.4477 16.5523 20 16 20H15Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

@ -1955,9 +1955,47 @@
"amountOnly": "المبلغ",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"paymentSuccessful": "تم الدفع بنجاح"
"paymentSuccessful": "تم الدفع بنجاح",
"typeAnyOffer": "اكتب أي عرض",
"validTill": "صالح حتى",
"buyNow": "اشتري الآن",
"availableAt": "متاح في",
"includedServices": "الخدمات المشمولة",
"off": "خصم",
"failedToLoadOfferDetails": "فشل في تحميل تفاصيل العرض",
"orderLabel": "الطلب",
"purchased": "تم الشراء",
"validUntil": "صالح حتى",
"viewOfferDetails": "عرض تفاصيل العرض",
"viewOrderDetails": "عرض تفاصيل الطلب",
"hospitalNA": "المستشفى: غير متوفر",
"hospitalLabel": "المستشفى",
"showLess": "عرض أقل",
"redeemed": "تم الاستخدام",
"partiallyRedeemed": "مستخدم جزئياً",
"available": "متاح",
"saveQRCode": "حفظ رمز الاستجابة السريعة",
"voucherNo": "رقم القسيمة",
"expiresOn": "ينتهي في",
"saveTheQRCode": "احفظ رمز الاستجابة السريعة",
"pleaseShowQRCodeInHospital": "يرجى حفظه على جهازك وإظهاره في المستشفى للمتابعة.",
"voucherCodeCopied": "تم نسخ رمز القسيمة إلى الحافظة",
"qrCodeNotAvailableToShare": "رمز الاستجابة السريعة غير متاح للمشاركة",
"failedToShareQRCode": "فشل في مشاركة رمز الاستجابة السريعة",
"failedToSaveQRCode": "فشل في حفظ رمز الاستجابة السريعة",
"qrCodeSavedSuccessfully": "تم حفظ رمز الاستجابة السريعة بنجاح",
"qrCodeSavedToDevice": "تم حفظ رمز الاستجابة السريعة على جهازك",
"viewOrderHistory": "عرض سجل الطلبات",
"qrCodeNotAvailable": "رمز الاستجابة السريعة غير متاح",
"errorLoadingQRCode": "خطأ في تحميل رمز الاستجابة السريعة",
"noOfferDetailsAvailable": "لا توجد تفاصيل عرض متاحة",
"noImagesAvailable": "لا توجد صور متاحة",
"checkOutThisAmazingOffer": "تحقق من هذا العرض المذهل!",
"offerPayment": "دفع العرض",
"unableToCreateOrder": "غير قادر على إنشاء الطلب. معلومات العرض أو المستخدم مفقودة.",
"failedToCreateOrder": "فشل في إنشاء الطلب. يرجى المحاولة مرة أخرى.",
"unableToCompletePayment": "غير قادر على إكمال الدفع. معلومات الطلب مفقودة.",
"paymentConfirmationFailed": "تم تسجيل الدفع لكن التأكيد فشل. يرجى الاتصال بالدعم."
}

@ -398,6 +398,8 @@ class AppAssets {
static const String habibLogoOnly = '$svgBasePath/habib logo_only.svg';
static const String descriptionIcon = '$svgBasePath/description_icon.svg';
static const String selectImageIcon = '$svgBasePath/select_image_icon.svg';
static const String qrCodeIcon = '$svgBasePath/qr_code.svg';
static const String hmgLogoSvg = '$svgBasePath/hmg_logo_icon.svg';
// PNGS //
static const String hmgLogo = '$pngBasePath/hmg_logo.png';

@ -1,12 +1,12 @@
import 'dart:convert';
class OffersAndDiscountsResponseModel {
List<OfferData> data;
Meta meta;
List<OfferData>? data;
Meta? meta;
OffersAndDiscountsResponseModel({
required this.data,
required this.meta,
this.data,
this.meta,
});
factory OffersAndDiscountsResponseModel.fromRawJson(String str) => OffersAndDiscountsResponseModel.fromJson(json.decode(str));
@ -14,41 +14,47 @@ class OffersAndDiscountsResponseModel {
String toRawJson() => json.encode(toJson());
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) => OffersAndDiscountsResponseModel(
data: List<OfferData>.from(json["data"].map((x) => OfferData.fromJson(x))),
meta: Meta.fromJson(json["meta"]),
data: json["data"] == null ? [] : List<OfferData>.from(json["data"]!.map((x) => OfferData.fromJson(x))),
meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
"data": data == null ? [] : List<dynamic>.from(data!.map((x) => x.toJson())),
"meta": meta?.toJson(),
};
}
class OfferData {
int id;
String ref;
String name;
String nameAr;
Category category;
List<Branch> branches;
double listPrice;
Currency currency;
String imageUrl;
DateTime validFrom;
DateTime validTo;
int? id;
String? ref;
String? name;
String? nameAr;
BranchApplicability? branchApplicability;
Category? category;
List<Branch>? branches;
double? listPrice;
double? vatExclusivePrice;
Currency? currency;
String? imageUrl;
DateTime? validFrom;
DateTime? validTo;
UsageValidity? usageValidity;
OfferData({
required this.id,
required this.ref,
required this.name,
required this.nameAr,
required this.category,
required this.branches,
required this.listPrice,
required this.currency,
required this.imageUrl,
required this.validFrom,
required this.validTo,
this.id,
this.ref,
this.name,
this.nameAr,
this.branchApplicability,
this.category,
this.branches,
this.listPrice,
this.vatExclusivePrice,
this.currency,
this.imageUrl,
this.validFrom,
this.validTo,
this.usageValidity,
});
factory OfferData.fromRawJson(String str) => OfferData.fromJson(json.decode(str));
@ -60,13 +66,16 @@ class OfferData {
ref: json["ref"],
name: json["name"],
nameAr: json["name_ar"],
category: Category.fromJson(json["category"]),
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
branchApplicability: branchApplicabilityValues.map[json["branch_applicability"]]!,
category: json["category"] == null ? null : Category.fromJson(json["category"]),
branches: json["branches"] == null ? [] : List<Branch>.from(json["branches"]!.map((x) => Branch.fromJson(x))),
listPrice: json["list_price"]?.toDouble(),
vatExclusivePrice: json["vat_exclusive_price"]?.toDouble(),
currency: currencyValues.map[json["currency"]]!,
imageUrl: json["image_url"],
validFrom: DateTime.parse(json["valid_from"]),
validTo: DateTime.parse(json["valid_to"]),
validFrom: json["valid_from"] == null ? null : DateTime.parse(json["valid_from"]),
validTo: json["valid_to"] == null ? null : DateTime.parse(json["valid_to"]),
usageValidity: json["usage_validity"] == null ? null : UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
@ -74,27 +83,40 @@ class OfferData {
"ref": ref,
"name": name,
"name_ar": nameAr,
"category": category.toJson(),
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
"branch_applicability": branchApplicabilityValues.reverse[branchApplicability],
"category": category?.toJson(),
"branches": branches == null ? [] : List<dynamic>.from(branches!.map((x) => x.toJson())),
"list_price": listPrice,
"vat_exclusive_price": vatExclusivePrice,
"currency": currencyValues.reverse[currency],
"image_url": imageUrl,
"valid_from": "${validFrom.year.toString().padLeft(4, '0')}-${validFrom.month.toString().padLeft(2, '0')}-${validFrom.day.toString().padLeft(2, '0')}",
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"valid_from": "${validFrom!.year.toString().padLeft(4, '0')}-${validFrom!.month.toString().padLeft(2, '0')}-${validFrom!.day.toString().padLeft(2, '0')}",
"valid_to": "${validTo!.year.toString().padLeft(4, '0')}-${validTo!.month.toString().padLeft(2, '0')}-${validTo!.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity?.toJson(),
};
}
enum BranchApplicability {
ALL,
SELECTED
}
final branchApplicabilityValues = EnumValues({
"all": BranchApplicability.ALL,
"selected": BranchApplicability.SELECTED
});
class Branch {
int id;
String code;
String name;
String nameAr;
int? id;
String? code;
String? name;
String? nameAr;
Branch({
required this.id,
required this.code,
required this.name,
required this.nameAr,
this.id,
this.code,
this.name,
this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
@ -117,16 +139,16 @@ class Branch {
}
class Category {
int id;
bool code;
String name;
String nameAr;
int? id;
bool? code;
String? name;
String? nameAr;
Category({
required this.id,
required this.code,
required this.name,
required this.nameAr,
this.id,
this.code,
this.name,
this.nameAr,
});
factory Category.fromRawJson(String str) => Category.fromJson(json.decode(str));
@ -156,17 +178,85 @@ final currencyValues = EnumValues({
"SAR": Currency.SAR
});
class UsageValidity {
Type? type;
dynamic days;
Rule? rule;
RuleAr? ruleAr;
dynamic redeemBy;
dynamic redeemByDisplay;
UsageValidity({
this.type,
this.days,
this.rule,
this.ruleAr,
this.redeemBy,
this.redeemByDisplay,
});
factory UsageValidity.fromRawJson(String str) => UsageValidity.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory UsageValidity.fromJson(Map<String, dynamic> json) => UsageValidity(
type: typeValues.map[json["type"]]!,
days: json["days"],
rule: ruleValues.map[json["rule"]]!,
ruleAr: ruleArValues.map[json["rule_ar"]]!,
redeemBy: json["redeem_by"],
redeemByDisplay: json["redeem_by_display"],
);
Map<String, dynamic> toJson() => {
"type": typeValues.reverse[type],
"days": days,
"rule": ruleValues.reverse[rule],
"rule_ar": ruleArValues.reverse[ruleAr],
"redeem_by": redeemBy,
"redeem_by_display": redeemByDisplay,
};
}
enum Rule {
REDEEM_BETWEEN_01072026_AND_02072026,
REDEEM_BETWEEN_08062026_AND_31082026
}
final ruleValues = EnumValues({
"Redeem between 01/07/2026 and 02/07/2026.": Rule.REDEEM_BETWEEN_01072026_AND_02072026,
"Redeem between 08/06/2026 and 31/08/2026.": Rule.REDEEM_BETWEEN_08062026_AND_31082026
});
enum RuleAr {
THE_0107202602072026,
THE_0806202631082026
}
final ruleArValues = EnumValues({
"صرف في الفترة من 01/07/2026 إلى 02/07/2026.": RuleAr.THE_0107202602072026,
"صرف في الفترة من 08/06/2026 إلى 31/08/2026.": RuleAr.THE_0806202631082026
});
enum Type {
OPEN_OFFER
}
final typeValues = EnumValues({
"open_offer": Type.OPEN_OFFER
});
class Meta {
int total;
int page;
int perPage;
int pages;
int? total;
int? page;
int? perPage;
int? pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
this.total,
this.page,
this.perPage,
this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));

@ -1,12 +1,12 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
List<Datum> data;
Meta meta;
List<Datum>? data;
Meta? meta;
OfferPurchaseHistoryResponseModel({
required this.data,
required this.meta,
this.data,
this.meta,
});
factory OfferPurchaseHistoryResponseModel.fromRawJson(String str) => OfferPurchaseHistoryResponseModel.fromJson(json.decode(str));
@ -14,43 +14,43 @@ class OfferPurchaseHistoryResponseModel {
String toRawJson() => json.encode(toJson());
factory OfferPurchaseHistoryResponseModel.fromJson(Map<String, dynamic> json) => OfferPurchaseHistoryResponseModel(
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
meta: Meta.fromJson(json["meta"]),
data: json["data"] == null ? [] : List<Datum>.from(json["data"]!.map((x) => Datum.fromJson(x))),
meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
"data": data == null ? [] : List<dynamic>.from(data!.map((x) => x.toJson())),
"meta": meta?.toJson(),
};
}
class Datum {
String voucherCode;
String state;
DateTime purchaseDate;
String? voucherCode;
String? state;
DateTime? purchaseDate;
dynamic redemptionDate;
String qrCode;
String qrCodeUrl;
String pdfUrl;
DateTime validTo;
UsageValidity usageValidity;
Offer offer;
String branchApplicability;
List<Branch> branches;
String? qrCode;
String? qrCodeUrl;
String? pdfUrl;
dynamic validTo;
UsageValidity? usageValidity;
Offer? offer;
String? branchApplicability;
List<Branch>? branches;
Datum({
required this.voucherCode,
required this.state,
required this.purchaseDate,
required this.redemptionDate,
required this.qrCode,
required this.qrCodeUrl,
required this.pdfUrl,
required this.validTo,
required this.usageValidity,
required this.offer,
required this.branchApplicability,
required this.branches,
this.voucherCode,
this.state,
this.purchaseDate,
this.redemptionDate,
this.qrCode,
this.qrCodeUrl,
this.pdfUrl,
this.validTo,
this.usageValidity,
this.offer,
this.branchApplicability,
this.branches,
});
factory Datum.fromRawJson(String str) => Datum.fromJson(json.decode(str));
@ -60,43 +60,43 @@ class Datum {
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
voucherCode: json["voucher_code"],
state: json["state"],
purchaseDate: DateTime.parse(json["purchase_date"]),
purchaseDate: json["purchase_date"] == null ? null : DateTime.parse(json["purchase_date"]),
redemptionDate: json["redemption_date"],
qrCode: json["qr_code"],
qrCodeUrl: json["qr_code_url"],
pdfUrl: json["pdf_url"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
offer: Offer.fromJson(json["offer"]),
validTo: json["valid_to"],
usageValidity: json["usage_validity"] == null ? null : UsageValidity.fromJson(json["usage_validity"]),
offer: json["offer"] == null ? null : Offer.fromJson(json["offer"]),
branchApplicability: json["branch_applicability"],
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
branches: json["branches"] == null ? [] : List<Branch>.from(json["branches"]!.map((x) => Branch.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"voucher_code": voucherCode,
"state": state,
"purchase_date": purchaseDate.toIso8601String(),
"purchase_date": purchaseDate?.toIso8601String(),
"redemption_date": redemptionDate,
"qr_code": qrCode,
"qr_code_url": qrCodeUrl,
"pdf_url": pdfUrl,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
"offer": offer.toJson(),
"valid_to": validTo,
"usage_validity": usageValidity?.toJson(),
"offer": offer?.toJson(),
"branch_applicability": branchApplicability,
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
"branches": branches == null ? [] : List<dynamic>.from(branches!.map((x) => x.toJson())),
};
}
class Branch {
String code;
String name;
String nameAr;
String? code;
String? name;
String? nameAr;
Branch({
required this.code,
required this.name,
required this.nameAr,
this.code,
this.name,
this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
@ -117,20 +117,20 @@ class Branch {
}
class Offer {
int id;
String name;
String nameAr;
String branchApplicability;
DateTime validTo;
UsageValidity usageValidity;
int? id;
String? name;
String? nameAr;
String? branchApplicability;
DateTime? validTo;
UsageValidity? usageValidity;
Offer({
required this.id,
required this.name,
required this.nameAr,
required this.branchApplicability,
required this.validTo,
required this.usageValidity,
this.id,
this.name,
this.nameAr,
this.branchApplicability,
this.validTo,
this.usageValidity,
});
factory Offer.fromRawJson(String str) => Offer.fromJson(json.decode(str));
@ -142,8 +142,8 @@ class Offer {
name: json["name"],
nameAr: json["name_ar"],
branchApplicability: json["branch_applicability"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
validTo: json["valid_to"] == null ? null : DateTime.parse(json["valid_to"]),
usageValidity: json["usage_validity"] == null ? null : UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
@ -151,26 +151,26 @@ class Offer {
"name": name,
"name_ar": nameAr,
"branch_applicability": branchApplicability,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
"valid_to": "${validTo!.year.toString().padLeft(4, '0')}-${validTo!.month.toString().padLeft(2, '0')}-${validTo!.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity?.toJson(),
};
}
class UsageValidity {
String type;
String? type;
dynamic days;
String rule;
String ruleAr;
DateTime redeemBy;
String redeemByDisplay;
String? rule;
String? ruleAr;
dynamic redeemBy;
dynamic redeemByDisplay;
UsageValidity({
required this.type,
required this.days,
required this.rule,
required this.ruleAr,
required this.redeemBy,
required this.redeemByDisplay,
this.type,
this.days,
this.rule,
this.ruleAr,
this.redeemBy,
this.redeemByDisplay,
});
factory UsageValidity.fromRawJson(String str) => UsageValidity.fromJson(json.decode(str));
@ -182,7 +182,7 @@ class UsageValidity {
days: json["days"],
rule: json["rule"],
ruleAr: json["rule_ar"],
redeemBy: DateTime.parse(json["redeem_by"]),
redeemBy: json["redeem_by"],
redeemByDisplay: json["redeem_by_display"],
);
@ -191,22 +191,22 @@ class UsageValidity {
"days": days,
"rule": rule,
"rule_ar": ruleAr,
"redeem_by": "${redeemBy.year.toString().padLeft(4, '0')}-${redeemBy.month.toString().padLeft(2, '0')}-${redeemBy.day.toString().padLeft(2, '0')}",
"redeem_by": redeemBy,
"redeem_by_display": redeemByDisplay,
};
}
class Meta {
int total;
int page;
int perPage;
int pages;
int? total;
int? page;
int? perPage;
int? pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
this.total,
this.page,
this.perPage,
this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));

@ -34,14 +34,15 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String patientId, String mobileNumber) async {
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String nationalID, String mobileNumber) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
Map<String, dynamic> body = {
'patient_id': patientId,
'national_id': nationalID,
// 'patient_id': patientId,
'mobile_number': mobileNumber,
};

@ -1,4 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
@ -11,6 +16,7 @@ import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_purchases_history_response_model.dart' as history_model;
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:path_provider/path_provider.dart';
class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false;
@ -18,6 +24,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOfferDetailsLoading = false;
bool isAuthenticating = false;
bool isPurchasing = false;
bool isSaveQrCodePressed = false; // Track QR code capture state
List<int> selectedCategories = []; // Empty means 'All Offers'
String searchQuery = '';
@ -32,14 +39,156 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
PomsAuthTokenModel? pomsAuthToken;
OfferPurchaseResponseModel? lastPurchaseResponse;
// Image slider properties
Timer? _autoPlayTimer;
double _progress = 0.0;
bool _isPaused = false;
int _currentPage = 0;
List<String> _promoImages = [];
static const Duration _pageDuration = Duration(seconds: 5);
static const Duration _timerInterval = Duration(milliseconds: 50);
// Getters for slider state
double get progress => _progress;
bool get isPaused => _isPaused;
int get currentPage => _currentPage;
List<String> get promoImages => _promoImages;
OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo,
required this.errorHandlerService,
}) {}
});
@override
void dispose() {
stopAutoPlayTimer();
super.dispose();
}
// For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading;
// Initialize slider with page controller and initial index
void initializeSlider(int initialIndex) {
// Stop any existing timer
stopAutoPlayTimer();
// Clear previous images and state
_promoImages.clear();
_progress = 0.0;
_isPaused = false;
_currentPage = initialIndex;
notifyListeners();
}
// Collect all images from the offer details
List<String> getAllImageUrls(OffersAndDiscountsDetailedResponseModel offer) {
List<String> allImages = [];
// Add main images
if (offer.images.main.isNotEmpty) {
allImages.addAll(offer.images.main.map((banner) => banner.url));
}
// Add banner images
if (offer.images.banners.isNotEmpty) {
allImages.addAll(offer.images.banners.map((banner) => banner.url));
}
// Add detail images
if (offer.images.details.isNotEmpty) {
allImages.addAll(offer.images.details.map((banner) => banner.url));
}
// If no images from the images object, use the main imageUrl
if (allImages.isEmpty && offer.imageUrl.isNotEmpty) {
allImages.add(offer.imageUrl);
}
_promoImages = allImages;
print('📸 Loaded ${_promoImages.length} images for slider');
notifyListeners();
return allImages;
}
// Start auto-play timer
void startAutoPlayTimer() {
_autoPlayTimer?.cancel();
_progress = 0.0;
print('🎬 Starting auto-play timer with ${_promoImages.length} images');
_autoPlayTimer = Timer.periodic(_timerInterval, (timer) {
if (!_isPaused && _promoImages.isNotEmpty) {
_progress += _timerInterval.inMilliseconds / _pageDuration.inMilliseconds;
if (_progress >= 1.0) {
// Move to next page
_progress = 0.0;
final previousPage = _currentPage;
if (_currentPage < _promoImages.length - 1) {
_currentPage++;
} else {
// Last page reached, loop back to first page
_currentPage = 0;
}
print('🔄 Auto-switching from page $previousPage to $_currentPage');
}
notifyListeners();
}
});
}
// Stop auto-play timer
void stopAutoPlayTimer() {
_autoPlayTimer?.cancel();
_autoPlayTimer = null;
}
// Pause timer
void pauseTimer() {
_isPaused = true;
notifyListeners();
}
// Resume timer
void resumeTimer() {
_isPaused = false;
notifyListeners();
}
// Handle page change
void onPageChanged(int index) {
_currentPage = index;
_progress = 0.0;
notifyListeners();
}
// Navigate to next page
void nextPage() {
if (_currentPage < _promoImages.length - 1) {
_currentPage++;
} else {
// Loop back to first page
_currentPage = 0;
}
// Reset progress when manually changing page
_progress = 0.0;
notifyListeners();
}
// Reset slider state
void resetSliderState() {
stopAutoPlayTimer();
_currentPage = 0;
_progress = 0.0;
_isPaused = false;
_promoImages.clear();
notifyListeners();
}
initOffersAndDiscounts({bool forceRefresh = false}) {
// Only clear data if force refresh, otherwise keep existing data
if (forceRefresh) {
@ -55,6 +204,14 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
getOffersAndDiscounts();
}
setIsoffersListLoading(bool val) {
if (val) {
isOffersLoading = true;
}
isOffersLoading = val;
notifyListeners();
}
// Method to force refresh the offers list
Future<void> refreshOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
offersList.clear();
@ -102,10 +259,10 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners();
final result = await offersAndDiscountsRepo.authenticatePatient(
authenticatedUser.patientId.toString(),
authenticatedUser.patientIdentificationNo.toString(),
// "4774103",
// "2001273",
authenticatedUser.mobileNumber ?? "",
authenticatedUser.mobileNumber.toString(),
);
result.fold(
@ -287,8 +444,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
final Map<int, Category> categoriesMap = {};
for (var offer in offersList) {
if (!categoriesMap.containsKey(offer.category.id)) {
categoriesMap[offer.category.id] = offer.category;
if (!categoriesMap.containsKey(offer.category!.id)) {
categoriesMap[offer.category!.id!] = offer.category!;
}
}
@ -328,7 +485,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
if (hasExistingData) {
// Get new items that don't already exist in the list
final existingIds = offersList.map((e) => e.id).toSet();
final newItems = response.data!.data.where((item) => !existingIds.contains(item.id)).toList();
final newItems = response.data!.data!.where((item) => !existingIds.contains(item.id)).toList();
// Append only new items
if (newItems.isNotEmpty) {
@ -336,7 +493,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}
} else {
// First load - replace the list
offersList = response.data!.data;
offersList = response.data!.data!;
}
meta = response.data!.meta;
}
@ -349,6 +506,13 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}
Future<void> getOfferDetails(int offerId, {Function(dynamic)? onSuccess, Function(String)? onError}) async {
// Clear previous offer details and images before loading new ones
offerDetails = null;
_promoImages.clear();
stopAutoPlayTimer();
_progress = 0.0;
_currentPage = 0;
isOfferDetailsLoading = true;
notifyListeners();
@ -389,6 +553,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
return;
}
// Clear previous history data before loading new data
purchaseHistory = null;
isHistoryLoading = true;
notifyListeners();
@ -416,6 +582,17 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
);
}
// Initialize history page - clears data and fetches fresh data
void initializeHistory() {
// Clear previous data
purchaseHistory = null;
searchQuery = '';
notifyListeners();
// Fetch fresh data
getOffersAndDiscountsHistory();
}
List<history_model.Datum> get historyList {
return purchaseHistory?.data ?? [];
}
@ -426,15 +603,15 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
// Filter by categories (if any categories are selected)
if (selectedCategories.isNotEmpty) {
filtered = filtered.where((offer) {
return selectedCategories.contains(offer.category.id);
return selectedCategories.contains(offer.category!.id);
}).toList();
}
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
final titleMatch = offer.name.toLowerCase().contains(searchQuery.toLowerCase());
final titleArMatch = offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
final titleMatch = offer.name!.toLowerCase().contains(searchQuery.toLowerCase());
final titleArMatch = offer.nameAr!.toLowerCase().contains(searchQuery.toLowerCase());
return titleMatch || titleArMatch;
}).toList();
}
@ -448,13 +625,145 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((history) {
final offerNameMatch = history.offer.name.toLowerCase().contains(searchQuery.toLowerCase());
final offerNameArMatch = history.offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
final voucherCodeMatch = history.voucherCode.toLowerCase().contains(searchQuery.toLowerCase());
final offerNameMatch = history.offer!.name!.toLowerCase().contains(searchQuery.toLowerCase());
final offerNameArMatch = history.offer!.nameAr!.toLowerCase().contains(searchQuery.toLowerCase());
final voucherCodeMatch = history.voucherCode!.toLowerCase().contains(searchQuery.toLowerCase());
return offerNameMatch || offerNameArMatch || voucherCodeMatch;
}).toList();
}
return filtered;
}
/// Capture QR code screenshot and save to storage
Future<Map<String, dynamic>> captureAndSaveQRCode({
required GlobalKey screenshotKey,
required String voucherCode,
required bool isFromPaymentScreen,
required BuildContext context,
String? offerName,
String? offerNameAr,
}) async {
try {
final appState = getIt.get<AppState>();
// Capture the screenshot
final RenderRepaintBoundary boundary = screenshotKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage(pixelRatio: 3.0);
final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
// Always save to external storage (permanent storage)
final String? savedFilePath = await saveToExternalStorage(
imageBytes: pngBytes,
voucherCode: voucherCode,
context: context,
);
// Prepare share data
final displayOfferName = appState.getLanguageID() == 1 ? offerNameAr : offerName;
return {
'success': savedFilePath != null,
'filePath': savedFilePath,
'isFromPayment': isFromPaymentScreen,
'offerName': displayOfferName,
};
} catch (e) {
print('Error capturing screenshot: $e');
return {
'success': false,
'error': e.toString(),
};
}
}
/// Unified method to handle QR code capture and save/share with state management
Future<Map<String, dynamic>> handleQRCodeAction({
required GlobalKey screenshotKey,
required String voucherCode,
required bool shouldSaveToStorage,
required bool isFromPaymentScreen,
required BuildContext context,
String? offerName,
String? offerNameAr,
}) async {
try {
isSaveQrCodePressed = true;
notifyListeners();
await Future.delayed(const Duration(milliseconds: 500));
final result = await captureAndSaveQRCode(
screenshotKey: screenshotKey,
voucherCode: voucherCode,
isFromPaymentScreen: shouldSaveToStorage ? isFromPaymentScreen : false,
context: context,
offerName: offerName,
offerNameAr: offerNameAr);
// Reset the flag
isSaveQrCodePressed = false;
notifyListeners();
return result;
} catch (e) {
print('Error in handleQRCodeAction: $e');
// Reset the flag if error occurs
isSaveQrCodePressed = false;
notifyListeners();
return {
'success': false,
'error': e.toString(),
};
}
}
/// Save QR code to external storage (accessible by user)
Future<String?> saveToExternalStorage({
required Uint8List imageBytes,
required String voucherCode,
required BuildContext context,
}) async {
try {
final String filename = 'QR_Code_${voucherCode}_${DateTime.now().millisecondsSinceEpoch}.png';
Directory? directory;
String? folderPath;
if (Platform.isAndroid) {
// For Android: Use app-specific external storage (no permissions required)
// This directory is accessible via file managers in Android/data/[package]/files/Pictures
directory = await getExternalStorageDirectory();
if (directory != null) {
// Save to app-specific external storage Pictures folder
folderPath = '${directory.path}/Pictures/HMG_QR_Codes';
}
} else {
// For iOS, save to app documents directory (accessible via Files app)
directory = await getApplicationDocumentsDirectory();
folderPath = '${directory.path}/QR_Codes';
}
if (folderPath == null) {
print('Failed to get storage path');
return null;
}
final Directory saveDirectory = Directory(folderPath);
if (!await saveDirectory.exists()) {
await saveDirectory.create(recursive: true);
}
final String filepath = '$folderPath/$filename';
final File file = File(filepath);
await file.writeAsBytes(imageBytes);
print('QR Code saved to: $filepath');
return filepath;
} catch (e) {
print('Error saving to external storage: $e');
return null;
}
}
}

@ -1941,5 +1941,45 @@ abstract class LocaleKeys {
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
static const paymentSuccessful = 'paymentSuccessful';
static const typeAnyOffer = 'typeAnyOffer';
static const validTill = 'validTill';
static const buyNow = 'buyNow';
static const availableAt = 'availableAt';
static const includedServices = 'includedServices';
static const off = 'off';
static const failedToLoadOfferDetails = 'failedToLoadOfferDetails';
static const orderLabel = 'orderLabel';
static const purchased = 'purchased';
static const validUntil = 'validUntil';
static const viewOfferDetails = 'viewOfferDetails';
static const viewOrderDetails = 'viewOrderDetails';
static const hospitalNA = 'hospitalNA';
static const hospitalLabel = 'hospitalLabel';
static const showLess = 'showLess';
static const redeemed = 'redeemed';
static const partiallyRedeemed = 'partiallyRedeemed';
static const available = 'available';
static const saveQRCode = 'saveQRCode';
static const voucherNo = 'voucherNo';
static const expiresOn = 'expiresOn';
static const saveTheQRCode = 'saveTheQRCode';
static const pleaseShowQRCodeInHospital = 'pleaseShowQRCodeInHospital';
static const voucherCodeCopied = 'voucherCodeCopied';
static const qrCodeNotAvailableToShare = 'qrCodeNotAvailableToShare';
static const failedToShareQRCode = 'failedToShareQRCode';
static const failedToSaveQRCode = 'failedToSaveQRCode';
static const qrCodeSavedSuccessfully = 'qrCodeSavedSuccessfully';
static const qrCodeSavedToDevice = 'qrCodeSavedToDevice';
static const viewOrderHistory = 'viewOrderHistory';
static const qrCodeNotAvailable = 'qrCodeNotAvailable';
static const errorLoadingQRCode = 'errorLoadingQRCode';
static const noOfferDetailsAvailable = 'noOfferDetailsAvailable';
static const noImagesAvailable = 'noImagesAvailable';
static const checkOutThisAmazingOffer = 'checkOutThisAmazingOffer';
static const offerPayment = 'offerPayment';
static const unableToCreateOrder = 'unableToCreateOrder';
static const failedToCreateOrder = 'failedToCreateOrder';
static const unableToCompletePayment = 'unableToCompletePayment';
static const paymentConfirmationFailed = 'paymentConfirmationFailed';
}

@ -230,6 +230,8 @@ class _LandingPageState extends State<LandingPage> {
// Refresh Immediate LiveCare Data
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
offersAndDiscountsViewModel.setIsoffersListLoading(true);
offersAndDiscountsViewModel.initOffersAndDiscounts();
// appointmentRatingViewModel.getLastRatingAppointment(
// onSuccess: (response) {
@ -452,8 +454,102 @@ class _LandingPageState extends State<LandingPage> {
// Offers And Discounts Carousel - Auto-scrolling from right to left
//appState.isAuthenticated &&
appState.isEnabledOffersAndDiscountsCarousel && offersAndDiscountsViewModel.offersList.isNotEmpty
? Column(
appState.isEnabledOffersAndDiscountsCarousel
? Consumer<OffersAndDiscountsViewModel>(
builder: (context, offersVM, child) {
// Show loading state
if (offersVM.isOffersLoading && offersVM.offersList.isEmpty) {
return SizedBox(
height: 79.h,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 0.w),
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: 79.w,
height: 79.h,
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(13.r),
smoothness: 0.6,
side: BorderSide(color: Color(0xff2E3039), width: 2.w),
),
color: AppColors.whiteColor,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13.r),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: AppColors.textColor, width: 2.w),
borderRadius: BorderRadius.circular(11.r),
),
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(11.r),
child: Container(
color: AppColors.greyColor,
child: Icon(
Icons.local_offer,
size: 24.h,
color: AppColors.greyTextColor,
),
),
),
),
),
),
).toShimmer();
},
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<Color>(AppColors.primaryRedColor),
// ),
// ),
// ).paddingSymmetrical(24.h, 0.h),
// SizedBox(height: 18.h),
// ],
// );
}
// Show offers if available
if (offersVM.offersList.isNotEmpty) {
return Column(
children: [
SizedBox(height: 12.h),
Row(
@ -475,6 +571,12 @@ class _LandingPageState extends State<LandingPage> {
OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h),
SizedBox(height: 18.h),
],
);
}
// Hide if no offers and not loading
return SizedBox.shrink();
},
)
: SizedBox.shrink(),

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -6,7 +7,6 @@ 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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_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';
@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:shimmer/shimmer.dart';
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
final int offerId;
@ -33,55 +34,34 @@ class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
}
class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscountsFullScreenSwiperPage> {
late PageController _pageController;
late int _currentPage;
late OffersAndDiscountsViewModel viewModel;
late AppState appState;
List<String> _promoImages = []; // Changed from late to regular variable
@override
void initState() {
super.initState();
_currentPage = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
// Fetch offer details
// Fetch offer details and initialize slider
scheduleMicrotask(() {
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.getOfferDetails(widget.offerId);
});
}
// Collect all images from the offer details
List<String> _getAllImageUrls(OffersAndDiscountsDetailedResponseModel offer) {
List<String> allImages = [];
// Add main images
if (offer.images.main.isNotEmpty) {
allImages.addAll(offer.images.main.map((banner) => banner.url));
final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.initializeSlider(widget.initialIndex);
viewModel.getOfferDetails(widget.offerId, onSuccess: (_) {
// Load images from the offer details
if (viewModel.offerDetails != null) {
viewModel.getAllImageUrls(viewModel.offerDetails!);
// Start auto-play after images are loaded
if (viewModel.promoImages.isNotEmpty) {
viewModel.startAutoPlayTimer();
}
// Add banner images
if (offer.images.banners.isNotEmpty) {
allImages.addAll(offer.images.banners.map((banner) => banner.url));
}
// Add detail images
if (offer.images.details.isNotEmpty) {
allImages.addAll(offer.images.details.map((banner) => banner.url));
}
// If no images from the images object, use the main imageUrl
if (allImages.isEmpty && offer.imageUrl.isNotEmpty) {
allImages.add(offer.imageUrl);
}
return allImages;
});
});
}
@override
void dispose() {
_pageController.dispose();
final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.resetSliderState();
super.dispose();
}
@ -90,24 +70,12 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: const Color(0xFFADA6D0),
body: Consumer<OffersAndDiscountsViewModel>(
body: SafeArea(
child: Consumer<OffersAndDiscountsViewModel>(
builder: (context, vm, child) {
// Show loading state
// Show shimmer loading state
if (vm.isOfferDetailsLoading) {
return SafeArea(
child: Stack(
children: [
Center(child: Utils.getLoadingWidget()),
// Close button while loading
Positioned(
top: 26.h + 14.h,
right: 24.w,
child: _buildCloseButton(),
),
],
),
);
return _buildShimmerLoading();
}
final offer = vm.offerDetails;
@ -117,7 +85,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
children: [
Center(
child: Text(
'No offer details available',
LocaleKeys.noOfferDetailsAvailable.tr(context: context),
style: TextStyle(
fontSize: 16.f,
color: AppColors.whiteColor,
@ -126,7 +94,8 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
),
Positioned(
top: 26.h + 14.h,
right: 24.w,
right: appState.isArabic() ? null : 24.w,
left: appState.isArabic() ? 24.w : null,
child: _buildCloseButton(),
),
],
@ -134,11 +103,11 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
);
}
// Get all images from the offer
_promoImages = _getAllImageUrls(offer);
if (_promoImages.isEmpty) {
return SafeArea(
// Check if images are empty (show no images state)
if (vm.promoImages.isEmpty) {
return Container(
color: const Color(0xFFADA6D0),
child: SafeArea(
child: Stack(
children: [
Center(
@ -152,7 +121,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
),
SizedBox(height: 16.h),
Text(
'No images available',
LocaleKeys.noImagesAvailable.tr(context: context),
style: TextStyle(
fontSize: 16.f,
color: AppColors.whiteColor,
@ -168,117 +137,100 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
),
],
),
),
);
}
return SafeArea(
child: Stack(
children: [
// Main content area
Column(
// Build page with image background
return Stack(
children: [
SizedBox(height: 26.h),
// Page indicators
_buildPageIndicators(),
SizedBox(height: 70.h),
// Swipeable image section
Expanded(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemCount: _promoImages.length,
itemBuilder: (context, index) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h),
child: Transform.flip(
flipX: appState.isArabic(),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
// Blurred background image
Positioned.fill(
child: Utils.buildImgWithNetwork(
url: _promoImages[index],
url: vm.promoImages[vm.currentPage],
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
iconColor: AppColors.whiteColor,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_not_supported,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
Text(
'Failed to load image',
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
),
);
return Container(color: const Color(0xFFADA6D0));
},
),
),
// Black shadow overlay
Positioned.fill(
child: Container(
color: Colors.black.withValues(alpha: 0.3),
),
),
);
},
),
),
SizedBox(height: 70.h),
// Bottom action buttons
// Main content
GestureDetector(
onTapDown: (_) => vm.pauseTimer(),
onTapUp: (_) => vm.resumeTimer(),
onTapCancel: () => vm.resumeTimer(),
child: Stack(
children: [
Column(
children: [
SizedBox(height: 26.h),
_buildPageIndicators(vm),
Expanded(child: SizedBox()),
_buildBottomActions(vm),
SizedBox(height: 12.h),
],
),
// Close button (top right)
// Close button (top right for LTR, top left for RTL)
Positioned(
top: 26.h + 14.h, // After indicator spacing
right: 24.w,
right: appState.isArabic() ? null : 24.w,
left: appState.isArabic() ? 24.w : null,
child: _buildCloseButton()),
],
),
),
],
);
},
),
),
);
}
Widget _buildPageIndicators() {
Widget _buildPageIndicators(OffersAndDiscountsViewModel vm) {
final isRTL = appState.isArabic();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
textDirection: isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: List.generate(
_promoImages.length,
vm.promoImages.length,
(index) => Expanded(
child: Container(
margin: EdgeInsets.only(
right: index < _promoImages.length - 1 ? 4.w : 0,
right: isRTL ? 0 : (index < vm.promoImages.length - 1 ? 4.w : 0),
left: isRTL ? (index < vm.promoImages.length - 1 ? 4.w : 0) : 0,
),
height: 4.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.r),
// Fill bar if it's current page or any previous page
color: index <= _currentPage ? Colors.white : const Color(0x33000000), // 20% opacity
color: const Color(0x33000000), // 20% opacity background
),
child: FractionallySizedBox(
// For RTL, align progress from right to left
alignment: isRTL ? Alignment.centerRight : Alignment.centerLeft,
widthFactor: index < vm.currentPage
? 1.0 // Completed pages - full
: index == vm.currentPage
? vm.progress // Current page - animated progress
: 0.0, // Future pages - empty
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.r),
color: Colors.white,
),
),
),
),
),
@ -311,14 +263,17 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
}
Widget _buildBottomActions(OffersAndDiscountsViewModel vm) {
final isRTL = appState.isArabic();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
textDirection: isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
// Buy Now button (takes remaining space)
Expanded(
child: CustomButton(
text: 'Buy Now',
text: LocaleKeys.buyNow.tr(),
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () async {
@ -365,17 +320,11 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
SizedBox(width: 12.w),
// Next button
// Next button (with RTL-aware icon - in RTL, visually shows left arrow due to layout reversal)
_buildIconButton(
svgIcon: AppAssets.nextSwiper,
onTap: () {
if (_currentPage < _promoImages.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
onTap: () => vm.nextPage(),
flipForRTL: isRTL,
),
SizedBox(width: 12.w),
@ -384,7 +333,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
svgIcon: AppAssets.share,
onTap: () async {
// ignore: deprecated_member_use
await Share.share('Check out this amazing offer!');
await Share.share(LocaleKeys.checkOutThisAmazingOffer.tr(context: context));
}),
],
),
@ -395,6 +344,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
IconData? icon,
String? svgIcon,
required VoidCallback onTap,
bool flipForRTL = false,
}) {
return GestureDetector(
onTap: onTap,
@ -406,6 +356,10 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: flipForRTL
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(3.14159), // 180 degrees (pi radians) to flip horizontally
child: svgIcon != null
? Utils.buildSvgWithAssets(
icon: svgIcon,
@ -418,7 +372,146 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
color: AppColors.whiteColor,
size: 24.h,
),
)
: svgIcon != null
? Utils.buildSvgWithAssets(
icon: svgIcon,
iconColor: AppColors.whiteColor,
width: 24.h,
height: 24.h,
)
: Icon(
icon!,
color: AppColors.whiteColor,
size: 24.h,
),
),
),
);
}
Widget _buildShimmerLoading() {
return SafeArea(
child: Stack(
children: [
// Main shimmer content
Column(
children: [
SizedBox(height: 26.h),
// Page indicators shimmer
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: List.generate(
3, // Show 3 shimmer indicators
(index) => Expanded(
child: Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
margin: EdgeInsets.only(
right: appState.isArabic() ? 0 : (index < 2 ? 4.w : 0),
left: appState.isArabic() ? (index < 2 ? 4.w : 0) : 0,
),
height: 4.h,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(2.r), color: Colors.white),
),
),
),
),
),
),
SizedBox(height: 70.h),
// Large image shimmer in center
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h),
child: Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
),
),
),
),
SizedBox(height: 70.h),
// Bottom action buttons shimmer
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
children: [
// Buy Now button shimmer
Expanded(
child: Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
height: 56.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
),
),
),
SizedBox(width: 12.w),
// Next button shimmer
Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: 56.w,
height: 56.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
),
),
SizedBox(width: 12.w),
// Share button shimmer
Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: 56.w,
height: 56.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
),
),
],
),
),
SizedBox(height: 12.h),
],
),
// Close button (top right)
Positioned(
top: 26.h + 14.h,
right: appState.isArabic() ? null : 24.w,
left: appState.isArabic() ? 24.w : null,
child: _buildCloseButton(),
),
],
),
);
}

@ -1,6 +1,10 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.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/size_utils.dart';
@ -8,15 +12,23 @@ 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/offers_and_discounts/models/offers_purchases_history_response_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_discounts_history_page.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';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
class OffersAndDiscountsSuccessQrPage extends StatefulWidget {
final Datum purchaseData;
final bool isFromPaymentScreen;
const OffersAndDiscountsSuccessQrPage({super.key, required this.purchaseData});
const OffersAndDiscountsSuccessQrPage({super.key, required this.purchaseData, required this.isFromPaymentScreen});
@override
State<OffersAndDiscountsSuccessQrPage> createState() => _OffersAndDiscountsSuccessQrPageState();
@ -24,267 +36,289 @@ class OffersAndDiscountsSuccessQrPage extends StatefulWidget {
class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSuccessQrPage> {
late AppState appState;
String _formatDate(DateTime date) {
try {
return DateFormat('dd MMM yyyy, hh:mm a').format(date);
} catch (e) {
return date.toString();
}
}
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
String? qrCodeFilePath; // Store the QR code file path for sharing
final GlobalKey _screenshotKey = GlobalKey();
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
switch (status) {
case 'purchased':
return AppColors.successColor;
case 'redeemed':
case 'used':
return AppColors.warningColor;
return AppColors.greyTextColor;
case 'partially_redeemed':
return AppColors.alertColor;
case 'expired':
return AppColors.errorColor;
case 'active':
case 'valid':
return AppColors.successColor;
default:
return AppColors.greyTextColor;
return AppColors.alertColor;
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return LocaleKeys.active.tr(context: context);
switch (status) {
case 'purchased':
return LocaleKeys.purchased.tr(context: context);
case 'redeemed':
case 'used':
return "Redeemed";
return LocaleKeys.redeemed.tr(context: context);
case 'partially_redeemed':
return LocaleKeys.partiallyRedeemed.tr(context: context);
case 'expired':
return LocaleKeys.expired.tr(context: context);
case 'active':
case 'valid':
return LocaleKeys.active.tr(context: context);
default:
return status.toUpperCase();
return "";
}
}
void _copyVoucherCode() {
Clipboard.setData(ClipboardData(text: widget.purchaseData.voucherCode));
Utils.showToast("Voucher code copied to clipboard");
/// Handle QR code action by delegating to view model
Future<void> _handleQRCodeAction({required bool isShareAction}) async {
try {
final result = await offersAndDiscountsViewModel.handleQRCodeAction(
screenshotKey: _screenshotKey,
voucherCode: widget.purchaseData.voucherCode!,
shouldSaveToStorage: true, // Always save to permanent storage
isFromPaymentScreen: isShareAction ? false : widget.isFromPaymentScreen, // Share action never pops
context: context,
offerName: widget.purchaseData.offer!.name,
offerNameAr: widget.purchaseData.offer!.nameAr,
);
if (!mounted) return;
if (result['success']) {
if (isShareAction) {
// Share action: Saved to permanent storage, now share the file
final xFile = XFile(result['filePath']);
final offerName = result['offerName'];
// ignore: deprecated_member_use
await Share.shareXFiles(
[xFile],
text: '${LocaleKeys.saveQRCode.tr(context: context)} - ${LocaleKeys.orderLabel.tr(context: context)} #${widget.purchaseData.voucherCode} - $offerName',
subject: LocaleKeys.orderDetails.tr(context: context),
);
} else {
// Save action: Show success and optionally navigate
if (result['isFromPayment']) {
// From payment screen: Show success bottom sheet and navigate to history
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.qrCodeSavedSuccessfully.tr(context: context)),
isCloseButtonVisible: true,
isDismissible: true,
isFullScreen: false,
isAutoDismiss: true,
callBackFunc: () {
// Navigate after bottom sheet closes
if (mounted) {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsHistoryPage()));
}
},
);
} else {
// Not from payment screen: Show success bottom sheet and stay on page
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.qrCodeSavedSuccessfully.tr(context: context)),
isCloseButtonVisible: true,
isDismissible: true,
isFullScreen: false,
isAutoDismiss: true,
);
}
}
} else {
Utils.showToast(LocaleKeys.failedToSaveQRCode.tr(context: context));
}
} catch (e) {
print('Error in _handleQRCodeAction: $e');
Utils.showToast(LocaleKeys.failedToSaveQRCode.tr(context: context));
}
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
print(widget.isFromPaymentScreen);
return Consumer<OffersAndDiscountsViewModel>(
builder: (context, viewModel, child) {
offersAndDiscountsViewModel = viewModel;
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.paymentSuccessful.tr(context: context),
title: LocaleKeys.orderDetails.tr(context: context),
trailing: Utils.buildSvgWithAssets(icon: AppAssets.share, iconColor: AppColors.blackColor).onPress(() {
_handleQRCodeAction(isShareAction: true); // Share action - saves and shares
}),
bottomChild: CustomButton(
text: LocaleKeys.saveQRCode.tr(context: context),
onPressed: () {
_handleQRCodeAction(isShareAction: false); // Save action - saves and shows success
},
backgroundColor: AppColors.successColor,
borderColor: AppColors.successColor,
textColor: AppColors.whiteColor,
icon: AppAssets.qrCodeIcon,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
).paddingSymmetrical(24.h, 24.h),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 24.h),
// Success Icon
Container(
width: 80.h,
height: 80.h,
decoration: BoxDecoration(
color: AppColors.successColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.check_circle,
size: 48.h,
color: AppColors.successColor,
),
),
SizedBox(height: 16.h),
LocaleKeys.paymentSuccessful.tr(context: context).toText24(isBold: true, fontWeight: FontWeight.w700),
SizedBox(height: 8.h),
"Your offer has been purchased successfully!".toText14(color: AppColors.greyTextColor),
SizedBox(height: 32.h),
// Order Reference Card (QR Code placeholder)
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
//Save Qr Screenshot Area Start
RepaintBoundary(
key: _screenshotKey,
child: Container(
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Column(
children: [
LocaleKeys.orderNumber.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(24.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.h),
border: Border.all(color: AppColors.greyColor.withValues(alpha: 0.3)),
SizedBox(height: 24.h),
Utils.buildSvgWithAssets(icon: AppAssets.hmgLogoSvg, width: 48.w, height: 48.h),
SizedBox(height: 6.h),
_buildQRCode().paddingSymmetrical(0.h, 0.h),
AppCustomChipWidget(
labelText: "${LocaleKeys.expiresOn.tr(context: context)}: ${Utils.formatDateToDisplay(widget.purchaseData.offer!.validTo.toString())}",
backgroundColor: AppColors.lightRedButtonColor,
textColor: AppColors.primaryRedColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
isEnglishOnly: true,
),
child: Column(
children: [
// QR Code Placeholder (can be replaced with actual QR code widget later)
Container(
width: 200.h,
height: 200.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(8.h),
SizedBox(
height: 6.h,
),
child: Center(
child: Column(
if (viewModel.isSaveQrCodePressed) ...[
Row(
mainAxisAlignment: MainAxisAlignment.center,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
Icon(
Icons.qr_code,
size: 80.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
"QR Code".toText12(color: AppColors.greyTextColor),
("${LocaleKeys.voucherNo.tr(context: context)}: ").toText14(isBold: true, weight: FontWeight.w700),
"${widget.purchaseData.voucherCode}".toText14(isBold: true, weight: FontWeight.w700),
],
),
),
),
SizedBox(height: 16.h),
widget.purchaseData.voucherCode.toText20(isBold: true, weight: FontWeight.w700),
SizedBox(height: 8.h),
GestureDetector(
onTap: _copyVoucherCode,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.copy, size: 16.h, color: AppColors.primaryRedColor),
SizedBox(width: 4.w),
"Copy".toText12(color: AppColors.primaryRedColor),
],
),
),
SizedBox(height: 24.h),
],
),
),
SizedBox(height: 12.h),
"Show this at the facility to redeem your offer".toText12(color: AppColors.greyTextColor),
],
).paddingSymmetrical(24.h, 24.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
// Save Qr Screenshot Area End
// Order Details Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
Row(
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
LocaleKeys.orderDetails.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
("${LocaleKeys.voucherNo.tr(context: context)}: ").toText14(isBold: true, weight: FontWeight.w700),
"${widget.purchaseData.voucherCode}".toText14(isBold: true, weight: FontWeight.w700),
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.h, vertical: 6.h),
decoration: BoxDecoration(
color: _getStatusColor(widget.purchaseData.state).withValues(alpha: 0.1),
color: _getStatusColor(widget.purchaseData.state!).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.h),
),
child: _getStatusText(widget.purchaseData.state).toText12(
child: _getStatusText(widget.purchaseData.state!).toText10(
isBold: true,
color: _getStatusColor(widget.purchaseData.state),
color: _getStatusColor(widget.purchaseData.state!),
),
),
],
),
SizedBox(height: 20.h),
// Purchase Date
_buildDetailRow(
LocaleKeys.date.tr(context: context),
_formatDate(widget.purchaseData.purchaseDate),
),
SizedBox(height: 12.h),
// Valid Until
_buildDetailRow(
"Valid Until",
_formatDate(widget.purchaseData.validTo),
),
SizedBox(height: 12.h),
// Offer Name
_buildDetailRow(
"Offer Name",
appState.getLanguageID() == 1 ? widget.purchaseData.offer.nameAr : widget.purchaseData.offer.name,
),
SizedBox(height: 12.h),
Divider(color: AppColors.greyColor.withValues(alpha: 0.3)),
SizedBox(height: 12.h),
// Show offer description if available (note: Datum's Offer doesn't have description)
if (widget.purchaseData.usageValidity.rule.isNotEmpty) ...[
LocaleKeys.description.tr(context: context).toText16(isBold: true, weight: FontWeight.w700),
SizedBox(height: 8.h),
(appState.getLanguageID() == 1 ? widget.purchaseData.usageValidity.ruleAr : widget.purchaseData.usageValidity.rule).toText14(color: AppColors.greyTextColor),
],
SizedBox(height: 8.h),
LocaleKeys.saveTheQRCode.tr(context: context).toText21(isBold: true, weight: FontWeight.w700),
LocaleKeys.pleaseShowQRCodeInHospital.tr(context: context).toText12(isBold: true, fontWeight: FontWeight.w700),
],
).paddingSymmetrical(24.h, 24.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 32.h),
// Back to Home Button
CustomButton(
text: LocaleKeys.done.tr(context: context),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
],
),
),
),
);
},
);
}
Widget _buildDetailRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
Widget _buildQRCode() {
if (widget.purchaseData.qrCode == null || widget.purchaseData.qrCode!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 2,
child: label.toText14(color: AppColors.greyTextColor),
),
SizedBox(width: 16.w),
Expanded(
flex: 3,
child: Align(
alignment: Alignment.centerRight,
child: value.toText14(isBold: true),
),
Icon(Icons.qr_code_2, size: 48.h, color: AppColors.greyTextColor),
SizedBox(height: 8.h),
LocaleKeys.qrCodeNotAvailable.tr(context: context).toText12(color: AppColors.greyTextColor),
],
),
);
}
return FutureBuilder<String>(
future: Utils.createFileFromString(widget.purchaseData.qrCode!, "png"),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(color: AppColors.primaryRedColor),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48.h, color: AppColors.errorColor),
SizedBox(height: 8.h),
LocaleKeys.errorLoadingQRCode.tr(context: context).toText12(color: AppColors.errorColor),
],
),
);
}
if (snapshot.hasData && snapshot.data != null) {
qrCodeFilePath = snapshot.data;
return ClipRRect(
borderRadius: BorderRadius.circular(8.h),
child: Image.file(File(snapshot.data!), width: MediaQuery.of(context).size.width * 0.6.w, height: MediaQuery.of(context).size.width * 0.6.h, fit: BoxFit.cover),
);
}
return SizedBox();
},
);
}
}

@ -64,6 +64,23 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
super.initState();
}
/// Get the correct payment amount based on user type
/// Saudi users (patientType == 1) pay without VAT, non-Saudi users pay with VAT
double getPaymentAmount() {
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
final authenticatedUser = appState.getAuthenticatedUser();
if (selectedOffer == null || authenticatedUser == null) {
return 0.0;
}
// If Saudi user (patientType == 1), return price without VAT
// Otherwise, return full price with VAT
return authenticatedUser.patientType == 1
? selectedOffer.vatExclusivePrice
: selectedOffer.listPrice;
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
@ -116,7 +133,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.price.tr(context: context).toText14(color: AppColors.greyTextColor),
"${selectedOffer.listPrice} ${selectedOffer.currency}".toText16(isBold: true, color: AppColors.primaryRedColor),
"${selectedOffer.vatExclusivePrice} ${selectedOffer.currency}".toText16(isBold: true, color: AppColors.primaryRedColor),
],
),
],
@ -173,8 +190,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.vatExclusivePrice,
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
@ -194,7 +211,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.vatExclusivePrice.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
@ -212,7 +229,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
);
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("mada", selectedOffer.vatExclusivePrice);
openPaymentURL("mada", getPaymentAmount());
}
}
}),
@ -271,8 +288,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.vatExclusivePrice,
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
@ -292,7 +309,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.vatExclusivePrice.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
@ -310,7 +327,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
);
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("visa", selectedOffer.vatExclusivePrice);
openPaymentURL("visa", getPaymentAmount());
}
}
}),
@ -343,8 +360,14 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.vat15.tr().toText14(isBold: true, color: AppColors.greyTextColor),
// Check if user is Saudi (patientType == 1 means Saudi)
Utils.getPaymentAmountWithSymbol(
(selectedOffer.vatInclusivePrice - selectedOffer.vatExclusivePrice).toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13,
(appState.getAuthenticatedUser()?.patientType == 1
? "0.00"
: (selectedOffer.vatInclusivePrice - selectedOffer.vatExclusivePrice).toStringAsFixed(2))
.toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
@ -353,7 +376,15 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: selectedOffer.currency == "SAR"),
// If Saudi user (patientType == 1), show price without VAT, otherwise show full price with VAT
Utils.getPaymentAmountWithSymbol(
(appState.getAuthenticatedUser()?.patientType == 1
? selectedOffer.vatExclusivePrice.toString()
: selectedOffer.listPrice.toString())
.toText24(isBold: true),
AppColors.blackColor,
17,
isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
Platform.isIOS
@ -361,7 +392,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
.paddingSymmetrical(24.h, 0.h)
.onPress(() async {
if (Utils.havePrivilege(103)) {
await startApplePay(selectedOffer.vatExclusivePrice);
await startApplePay(getPaymentAmount());
} else {
// Create order first for non-privileged Apple Pay
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
@ -374,7 +405,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
transID = orderRef;
LoaderBottomSheet.hideLoader();
openPaymentURL("ApplePay", selectedOffer.vatExclusivePrice);
openPaymentURL("ApplePay", getPaymentAmount());
}
})
: SizedBox(height: 12.h),
@ -554,7 +585,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa;
applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(DateTime.now());
applePayInsertRequest.appointmentNo = 0;
applePayInsertRequest.orderDescription = "Offer Payment";
applePayInsertRequest.orderDescription = LocaleKeys.offerPayment.tr(context: context);
applePayInsertRequest.liveServiceID = selectedOffer.id.toString();
applePayInsertRequest.latitude = "0.0";
applePayInsertRequest.longitude = "0.0";
@ -581,7 +612,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Offer Payment", amount);
paytabsViewModel.setPaymentConfiguration(LocaleKeys.offerPayment.tr(context: context), amount);
paytabsViewModel.startApplePayPayment(
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
@ -591,7 +622,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
payfortViewModel.paymentWithApplePay(
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
orderDescription: "Offer Payment",
orderDescription: LocaleKeys.offerPayment.tr(context: context),
orderAmount: amount,
merchantReference: transID,
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier,
@ -707,7 +738,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
orderId: lastPurchaseResponse.orderId,
status: paymentStatus,
paymentRef: transID,
amount: selectedOffer.vatExclusivePrice.toString(),
amount: lastPurchaseResponse.amount.toString(),
currency: selectedOffer.currency,
onSuccess: (response) async {
// Payment result sent successfully, now fetch purchase history
@ -717,14 +748,14 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
// Find the purchased offer in the history by matching the offer ID
final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere(
(item) => item.offer.id == selectedOffer.id,
(item) => item.offer!.id == selectedOffer.id,
orElse: () => offersAndDiscountsViewModel.historyList.first,
);
// Navigate to success QR page with purchase data
Navigator.of(context).pushReplacement(
CustomPageRoute(
page: OffersAndDiscountsSuccessQrPage(purchaseData: purchaseData),
page: OffersAndDiscountsSuccessQrPage(purchaseData: purchaseData, isFromPaymentScreen: true,),
),
);
},

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -18,8 +19,10 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class OffersAndDiscountsDetailedPage extends StatefulWidget {
final int offerId;
@ -33,15 +36,43 @@ class OffersAndDiscountsDetailedPage extends StatefulWidget {
class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetailedPage> {
late OffersAndDiscountsViewModel viewModel;
late AppState appState;
bool hasLoadingError = false;
@override
void initState() {
super.initState();
scheduleMicrotask(() {
viewModel.getOfferDetails(widget.offerId);
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
// Clear previous offer details to prevent showing old data
viewModel.offerDetails = null;
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
viewModel.getOfferDetails(
widget.offerId,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
setState(() {
hasLoadingError = false;
});
},
onError: (error) {
LoaderBottomSheet.hideLoader();
setState(() {
hasLoadingError = true;
});
},
);
});
}
@override
void dispose() {
// Optional: Clear offer details when leaving the page to save memory
// Uncomment if you want to clear cache on page exit
// viewModel.offerDetails = null;
super.dispose();
}
String _formatDateString(DateTime date) {
try {
return DateFormat('d MMM, yyyy').format(date);
@ -54,9 +85,9 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
String _getOfferStatus(DateTime validTo) {
DateTime now = DateTime.now();
if (now.isAfter(validTo)) {
return 'Expired';
return LocaleKeys.expired.tr(context: context);
} else {
return 'Active';
return LocaleKeys.active.tr(context: context);
}
}
@ -89,18 +120,69 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.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 (!isAuthenticated) {
return;
}
viewModel.setSelectedOffer(viewModel.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
).paddingSymmetrical(24.w, 24.h),
child: Consumer<OffersAndDiscountsViewModel>(
builder: (context, vm, child) {
if (vm.isOfferDetailsLoading) {
return Center(child: Utils.getLoadingWidget());
}
final offer = vm.offerDetails;
if (offer == null) {
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
// Show error if loading failed
if (hasLoadingError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64.h, color: AppColors.errorColor),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText16(isBold: true),
SizedBox(height: 8.h),
LocaleKeys.failedToLoadOfferDetails.tr(context: context).toText14(color: AppColors.greyTextColor),
],
),
);
}
print(vm.offerDetails!.toRawJson());
// Only show offer if it matches the requested offerId (prevents showing old cached data)
if (offer == null || offer.id != widget.offerId) {
return SizedBox.shrink(); // Return empty widget while loading
}
final status = _getOfferStatus(offer.validTo);
return SingleChildScrollView(
@ -111,12 +193,15 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
if (offer.images.banners.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork(
url: offer.images.banners.first.url,
iconColor: AppColors.blackBgColor,
child: Image.network(
offer.images.banners.first.url,
width: double.infinity,
height: 250.h,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildImageLoadingShimmer();
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 250.h,
@ -134,12 +219,15 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
else if (offer.imageUrl.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork(
url: offer.imageUrl,
iconColor: AppColors.blackBgColor,
child: Image.network(
offer.imageUrl,
width: double.infinity,
height: 250.h,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildImageLoadingShimmer();
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 250.h,
@ -164,13 +252,14 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
// Price Information
Row(
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
"Price: ".toText18(isBold: true, weight: FontWeight.w700),
"${offer.listPrice} ${offer.currency}".toText18(
color: AppColors.primaryRedColor,
isBold: true,
weight: FontWeight.w700,
),
"${LocaleKeys.price.tr(context: context)}: ".toText18(isBold: true, weight: FontWeight.w700),
Utils.getPaymentAmountWithSymbol(
((offer.listPrice).toStringAsFixed(2)).toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: offer.currency == "SAR")
],
),
@ -180,10 +269,11 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
Wrap(
spacing: 12.w,
runSpacing: 12.h,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
// Valid till chip
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.validTo)}",
labelText: "${LocaleKeys.validTill.tr(context: context)} ${_formatDateString(offer.validTo)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
@ -210,7 +300,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
// Branches Section
if (offer.branches.isNotEmpty) ...[
"Available at".toText18(
LocaleKeys.availableAt.tr(context: context).toText18(
isBold: true,
weight: FontWeight.w700,
),
@ -218,6 +308,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
Wrap(
spacing: 8.w,
runSpacing: 8.h,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: offer.branches.map((branch) {
return AppCustomChipWidget(
labelText: appState.getLanguageID() == 1 ? branch.nameAr : branch.name,
@ -247,7 +338,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
// Offer Lines Section
if (offer.offerLines.isNotEmpty) ...[
"Included Services".toText18(
LocaleKeys.includedServices.tr(context: context).toText18(
isBold: true,
weight: FontWeight.w700,
),
@ -271,12 +362,13 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
textDirection: appState.isArabic() ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
"Quantity: ${line.quantity}".toText14(
"${LocaleKeys.quantity.tr(context: context)}: ${line.quantity}".toText14(
color: AppColors.greyTextColor,
),
if (line.discountValue > 0)
"${line.discountValue}% OFF".toText14(
"${line.discountValue}% ${LocaleKeys.off.tr(context: context)}".toText14(
color: AppColors.primaryRedColor,
isBold: true,
),
@ -288,56 +380,27 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
}),
SizedBox(height: 24.h),
],
// Buy Now Button
CustomButton(
text: "Buy Now",
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) {
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
],
).paddingSymmetrical(24.h, 0.h),
);
},
),
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page
viewModel.setSelectedOffer(offer);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
// Helper method to build shimmer loading placeholder for images
Widget _buildImageLoadingShimmer() {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: double.infinity,
height: 250.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.h, 0.h),
);
},
),
);
}

@ -2,20 +2,27 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.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';
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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.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';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import '../../features/offers_and_discounts/models/offers_purchases_history_response_model.dart';
class OffersAndDiscountsHistoryPage extends StatefulWidget {
const OffersAndDiscountsHistoryPage({super.key});
@ -25,70 +32,102 @@ class OffersAndDiscountsHistoryPage extends StatefulWidget {
}
class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistoryPage> {
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
late AppState appState;
// Track which history items have expanded hospitals list
Set<String> expandedHospitals = {};
@override
void initState() {
super.initState();
scheduleMicrotask(() {
offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
final offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
offersAndDiscountsViewModel.initializeHistory();
});
super.initState();
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView(
title: LocaleKeys.order.tr(context: context),
isLeading: true,
child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
// Show loading state
if (offersAndDiscountVM.isHistoryLoading) {
return Container(
padding: EdgeInsets.all(24.h),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
return _buildLoadingState();
}
final historyList = offersAndDiscountVM.filteredHistory;
List<Datum> historyList = offersAndDiscountVM.filteredHistory;
if (historyList.isEmpty) {
return Container(
padding: EdgeInsets.all(24.h),
child: Column(
children: [
Icon(
Icons.history,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText18(isBold: true),
SizedBox(height: 8.h),
"You haven't purchased any offers yet.".toText14(color: AppColors.greyTextColor),
],
),
);
return Utils.getNoDataWidget(context);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: historyList.length,
padding: EdgeInsets.only(bottom: 50),
separatorBuilder: (context, index) => SizedBox(height: 16.h),
itemBuilder: (context, index) {
final history = historyList[index];
Datum history = historyList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: _buildHistoryCard(history),
),
),
);
},
),
],
);
}),
),
);
}
Widget _buildLoadingState() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 40.h),
child: Column(
children: List.generate(
3,
(index) => Container(
margin: EdgeInsets.only(bottom: 16.h),
child: Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
height: 200.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(20.r),
),
),
),
),
),
),
);
}
Widget _buildHistoryCard(Datum history) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 0.h),
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -103,9 +142,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: (appState.getLanguageID() == 1 ? history.offer.nameAr : history.offer.name).toText16(isBold: true),
),
(appState.getLanguageID() == 1 ? history.offer!.nameAr ?? '' : history.offer!.name ?? '').toText16(isBold: true),
],
),
SizedBox(height: 12.h),
@ -114,35 +151,31 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: "Order: ${history.voucherCode}",
labelText: "${LocaleKeys.orderLabel.tr(context: context)}: ${history.voucherCode}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
textColor: AppColors.textColor,
icon: AppAssets.my_sick_leave_icon,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Purchased: ${_formatDateChipFromDateTime(history.purchaseDate)}",
labelText: "${LocaleKeys.purchased.tr(context: context)}: ${_formatDateChipFromDateTime(history.purchaseDate!)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
textColor: AppColors.textColor,
icon: AppAssets.calendar,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Valid until: ${_formatDateChipFromDateTime(history.validTo)}",
history.offer!.validTo != null
? AppCustomChipWidget(
labelText: "${LocaleKeys.validUntil.tr(context: context)}: ${_formatDateChipFromDateTime(history.offer!.validTo!)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
textColor: AppColors.textColor,
icon: AppAssets.calendar,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
isEnglishOnly: true,
),
AppCustomChipWidget(
labelText:
"Hospital: ${appState.getLanguageID() == 1 ? history.branches.first.nameAr : history.branches.first.name} ${history.branches.length > 1 ? '(+${history.branches.length - 1} more)' : ''}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
)
: SizedBox(),
// Hospital chips - show multiple chips when expanded
...(_buildHospitalChips(history)),
],
),
SizedBox(height: 12.h),
@ -150,129 +183,248 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
children: [
Expanded(
child: CustomButton(
text: "View Offer Details",
onPressed: () {},
text: LocaleKeys.viewOfferDetails.tr(),
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(offerId: history.offer!.id!),
),
);
},
backgroundColor: AppColors.lightRedButtonColor,
borderColor: AppColors.lightRedButtonColor,
textColor: AppColors.primaryRedColor,
borderWidth: 0.w,
fontWeight: FontWeight.w500,
fontSize: 14.f,
height: 40.h,
),
),
SizedBox(
width: 9.w,
),
SizedBox(width: 9.w),
Expanded(
child: CustomButton(
text: "View Order Details",
text: LocaleKeys.viewOrderDetails.tr(),
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsSuccessQrPage(purchaseData: history)));
Navigator.of(context).push(CustomPageRoute(
page: OffersAndDiscountsSuccessQrPage(
purchaseData: history,
isFromPaymentScreen: false,
)));
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
fontWeight: FontWeight.w500,
fontSize: 14.f,
height: 40.h,
borderWidth: 1.w,
),
)
],
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h);
},
),
SizedBox(height: 24.h),
],
);
}),
),
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'completed':
case 'success':
case 'paid':
return AppColors.lightGreenColor;
case 'pending':
return AppColors.alertColor;
case 'cancelled':
case 'failed':
return AppColors.errorColor;
default:
return AppColors.greyTextColor;
String _formatDateChipFromDateTime(DateTime date) {
try {
return DateFormat('d MMM yyyy').format(date);
} catch (e) {
return date.toString();
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return LocaleKeys.active.tr(context: context);
case 'redeemed':
case 'used':
return "Redeemed";
case 'expired':
return LocaleKeys.expired.tr(context: context);
case 'pending':
return LocaleKeys.pending.tr(context: context);
case 'cancelled':
return LocaleKeys.cancelled.tr(context: context);
default:
return status.toUpperCase();
List<Widget> _buildHospitalChips(dynamic history) {
final isExpanded = expandedHospitals.contains(history.voucherCode);
final isArabic = appState.getLanguageID() == 1;
if (history.branches == null || history.branches!.isEmpty) {
return [
AppCustomChipWidget(
labelText: "Hospital: N/A",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.hospital,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
),
];
}
// If only one hospital, just show it
if (history.branches!.length == 1) {
final hospitalName = isArabic ? history.branches!.first.nameAr : history.branches!.first.name;
return [
AppCustomChipWidget(
labelText: "Hospital: $hospitalName",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.hospital,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
),
];
}
String _formatDateChipFromDateTime(DateTime date) {
try {
return DateFormat('d MMM yyyy').format(date);
} catch (e) {
return date.toString();
// Multiple hospitals
if (isExpanded) {
// Show each hospital as a separate chip
List<Widget> chips = history.branches!.map<Widget>((branch) {
final hospitalName = isArabic ? branch.nameAr : branch.name;
return AppCustomChipWidget(
labelText: "Hospital: $hospitalName",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.hospital,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
);
}).toList();
// Add a "Show less" chip at the end
chips.add(
AppCustomChipWidget(
labelText: "Show less",
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
textColor: AppColors.primaryRedColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
onChipTap: () {
setState(() {
expandedHospitals.remove(history.voucherCode);
});
},
),
);
return chips;
} else {
// Show collapsed view with count
final firstHospital = isArabic ? history.branches!.first.nameAr : history.branches!.first.name;
final remainingCount = history.branches!.length - 1;
return [
AppCustomChipWidget(
labelText: "Hospital: $firstHospital (+$remainingCount more)",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.hospital,
labelPadding: EdgeInsetsDirectional.only(start: 0.w, end: 8.w),
onChipTap: () {
setState(() {
expandedHospitals.add(history.voucherCode!);
});
},
),
];
}
}
String _getOfferStatus(dynamic history) {
try {
final now = DateTime.now();
final state = history.state?.toLowerCase() ?? '';
// Check if the offer has expired based on validTo date
if (now.isAfter(history.validTo)) {
// Check state first
switch (state) {
case 'purchased':
// Check if expired by date
if (history.validTo != null && now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
return LocaleKeys.purchased.tr(context: context);
case 'redeemed':
return LocaleKeys.redeemed.tr(context: context);
// Check state for active/available status
if (history.state.toLowerCase() == 'redeemed' || history.state.toLowerCase() == 'used') {
case 'partially_redeemed':
// Check if expired by date
if (history.validTo != null && now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
return LocaleKeys.partiallyRedeemed.tr(context: context);
if (history.state.toLowerCase() == 'active' || history.state.toLowerCase() == 'valid') {
return LocaleKeys.active.tr(context: context);
case 'expired':
return LocaleKeys.expired.tr(context: context);
case 'active':
case 'valid':
// Check if expired by date
if (history.validTo != null && now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
return LocaleKeys.active.tr(context: context);
// For pending or other states, show as available
return "Available";
default:
// Check if expired by date for unknown states
if (history.validTo != null && now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
return LocaleKeys.available.tr(context: context);
}
} catch (e) {
return LocaleKeys.active.tr(context: context);
}
}
Color _getOfferStatusBgColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
final state = history.state?.toLowerCase() ?? '';
final now = DateTime.now();
final isExpired = history.validTo != null && now.isAfter(history.validTo);
// If expired by date, show error color
if (isExpired && state != 'redeemed') {
return AppColors.errorColor.withValues(alpha: 0.1);
}
switch (state) {
case 'purchased':
return AppColors.successColor.withValues(alpha: 0.1);
} else if (status == LocaleKeys.expired.tr(context: context)) {
case 'redeemed':
return AppColors.greyTextColor.withValues(alpha: 0.1);
case 'partially_redeemed':
return AppColors.alertColor.withValues(alpha: 0.1);
case 'expired':
return AppColors.errorColor.withValues(alpha: 0.1);
} else {
case 'active':
case 'valid':
return AppColors.successColor.withValues(alpha: 0.1);
default:
return AppColors.alertColor.withValues(alpha: 0.1);
}
}
Color _getOfferStatusTextColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
final state = history.state?.toLowerCase() ?? '';
final now = DateTime.now();
final isExpired = history.validTo != null && now.isAfter(history.validTo);
// If expired by date, show error color
if (isExpired && state != 'redeemed') {
return AppColors.errorColor;
}
switch (state) {
case 'purchased':
return AppColors.successColor;
} else if (status == LocaleKeys.expired.tr(context: context)) {
case 'redeemed':
return AppColors.greyTextColor;
case 'partially_redeemed':
return AppColors.alertColor;
case 'expired':
return AppColors.errorColor;
} else {
case 'active':
case 'valid':
return AppColors.successColor;
default:
return AppColors.alertColor;
}
}

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -78,11 +79,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -176,7 +173,8 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
// Search Input
TextInputWidget(
labelText: LocaleKeys.search.tr(context: context),
hintText: LocaleKeys.search.tr(context: context),
hintText: LocaleKeys.typeAnyOffer.tr(),
hintColor: AppColors.textColor,
controller: _searchController,
onChange: (value) {
offersAndDiscountVM.setSearchQuery(value!);
@ -232,11 +230,11 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, crossAxisSpacing: 16.w, mainAxisSpacing: 16.h, childAspectRatio: 0.55),
itemCount: offersAndDiscountVM.filteredOffers.length,
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredGrid(
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
columnCount: 2,
child: ScaleAnimation(
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
@ -249,7 +247,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (offersAndDiscountVM.filteredOffers[index].imageUrl.isNotEmpty)
if (offersAndDiscountVM.filteredOffers[index].imageUrl != null)
Expanded(
flex: 3,
child: Stack(
@ -258,7 +256,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
child: SizedBox.expand(
child: Utils.buildImgWithNetwork(
url: offersAndDiscountVM.filteredOffers[index].imageUrl,
url: offersAndDiscountVM.filteredOffers[index].imageUrl ?? "",
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
@ -275,7 +273,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(
offerId: offersAndDiscountVM.filteredOffers[index].id,
offerId: offersAndDiscountVM.filteredOffers[index].id!,
),
),
);
@ -291,8 +289,10 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
appState.getLanguageID() == 1 ? offersAndDiscountVM.filteredOffers[index].nameAr : offersAndDiscountVM.filteredOffers[index].name,
SizedBox(
height: 40.h,
child: Text(
appState.getLanguageID() == 1 ? offersAndDiscountVM.filteredOffers[index].nameAr! : offersAndDiscountVM.filteredOffers[index].name!,
style: TextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w700,
@ -301,10 +301,11 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 8.h),
// Valid till chip
AppCustomChipWidget(
labelText: "Valid till ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].validTo.toString())}",
labelText: "${LocaleKeys.validTill.tr(context: context)} ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].validTo.toString())}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
@ -315,7 +316,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
),
SizedBox(height: 20.h),
CustomButton(
text: 'Buy Now',
text: LocaleKeys.buyNow.tr(context: context),
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () async {
@ -346,7 +347,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
// Fetch offer details
await offersAndDiscountVM.getOfferDetails(
offersAndDiscountVM.filteredOffers[index].id,
offersAndDiscountVM.filteredOffers[index].id!,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
if (offersAndDiscountVM.offerDetails != null) {
@ -395,11 +396,3 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
);
}
}
// Navigator.of(context).push(
// CustomPageRoute(
// page: OffersAndDiscountsDetailedPage(
// offerId: offersAndDiscountVM.filteredOffers[index].id,
// ),
// ),
// );

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
@ -29,7 +31,9 @@ class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
reverse: isArabic, // Reverse scroll direction for RTL
child: Row(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
children: [
// Show loading skeleton chips
if (isLoading) ...[
@ -57,10 +61,10 @@ class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
// Dynamic category buttons
...categories
.map((category) => Padding(
padding: EdgeInsets.only(right: 12.w),
padding: EdgeInsetsDirectional.only(end: 12.w),
child: _buildOfferTypeCard(
context: context,
title: isArabic ? category.nameAr : category.name,
title: isArabic ? category.nameAr ?? "" : category.name ?? "",
categoryId: category.id,
isSelected: selectedCategories.contains(category.id),
),

@ -104,11 +104,11 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
itemCount: viewModel.offersList.length,
itemBuilder: (context, index) {
final offer = viewModel.offersList[index];
return _buildOfferItem(offer.imageUrl).onPress((){
return _buildOfferItem(offer.imageUrl ?? "").onPress(() {
Navigator.push(
context,
CustomPageRoute(
page: OfferAndDiscountsFullScreenSwiperPage(offerId: offer.id),
page: OfferAndDiscountsFullScreenSwiperPage(offerId: offer.id!),
),
);
});
@ -121,21 +121,19 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
}
Widget _buildSkeletonLoader() {
return SizedBox(
height: 79.h,
child: ListView.separated(
return ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 0.w),
itemCount: 5, // Show 5 skeleton items
itemCount: 5,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: AppColors.greyColor.withValues(alpha: 0.3),
highlightColor: AppColors.whiteColor.withValues(alpha: 0.5),
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: 79.w,
height: 79.h,
decoration: ShapeDecoration(
color: AppColors.greyColor,
color: AppColors.shimmerBaseColor,
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(13.r),
smoothness: 0.6,
@ -145,7 +143,6 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
);
},
separatorBuilder: (context, index) => SizedBox(width: 12.w),
),
);
}

Loading…
Cancel
Save