diff --git a/assets/images/svg/hmg_logo_icon.svg b/assets/images/svg/hmg_logo_icon.svg
new file mode 100644
index 00000000..36c5b5cc
--- /dev/null
+++ b/assets/images/svg/hmg_logo_icon.svg
@@ -0,0 +1,20 @@
+
diff --git a/assets/images/svg/qr_code.svg b/assets/images/svg/qr_code.svg
new file mode 100644
index 00000000..57ba13cd
--- /dev/null
+++ b/assets/images/svg/qr_code.svg
@@ -0,0 +1,12 @@
+
diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json
index d45804d3..f5bf0d5c 100644
--- a/assets/langs/ar-SA.json
+++ b/assets/langs/ar-SA.json
@@ -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": "تم تسجيل الدفع لكن التأكيد فشل. يرجى الاتصال بالدعم."
}
diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart
index 2e27d633..7819ef40 100644
--- a/lib/core/app_assets.dart
+++ b/lib/core/app_assets.dart
@@ -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';
diff --git a/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart b/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart
index f1e84717..b64479f1 100644
--- a/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart
+++ b/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart
@@ -1,12 +1,12 @@
import 'dart:convert';
class OffersAndDiscountsResponseModel {
- List data;
- Meta meta;
+ List? 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 json) => OffersAndDiscountsResponseModel(
- data: List.from(json["data"].map((x) => OfferData.fromJson(x))),
- meta: Meta.fromJson(json["meta"]),
+ data: json["data"] == null ? [] : List.from(json["data"]!.map((x) => OfferData.fromJson(x))),
+ meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]),
);
Map toJson() => {
- "data": List.from(data.map((x) => x.toJson())),
- "meta": meta.toJson(),
+ "data": data == null ? [] : List.from(data!.map((x) => x.toJson())),
+ "meta": meta?.toJson(),
};
}
class OfferData {
- int id;
- String ref;
- String name;
- String nameAr;
- Category category;
- List branches;
- double listPrice;
- Currency currency;
- String imageUrl;
- DateTime validFrom;
- DateTime validTo;
+ int? id;
+ String? ref;
+ String? name;
+ String? nameAr;
+ BranchApplicability? branchApplicability;
+ Category? category;
+ List? 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.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.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 toJson() => {
@@ -74,27 +83,40 @@ class OfferData {
"ref": ref,
"name": name,
"name_ar": nameAr,
- "category": category.toJson(),
- "branches": List.from(branches.map((x) => x.toJson())),
+ "branch_applicability": branchApplicabilityValues.reverse[branchApplicability],
+ "category": category?.toJson(),
+ "branches": branches == null ? [] : List.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 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 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));
@@ -198,4 +288,4 @@ class EnumValues {
reverseMap = map.map((k, v) => MapEntry(v, k));
return reverseMap;
}
-}
\ No newline at end of file
+}
diff --git a/lib/features/offers_and_discounts/models/offers_purchases_history_response_model.dart b/lib/features/offers_and_discounts/models/offers_purchases_history_response_model.dart
index f884ab66..f3ea2850 100644
--- a/lib/features/offers_and_discounts/models/offers_purchases_history_response_model.dart
+++ b/lib/features/offers_and_discounts/models/offers_purchases_history_response_model.dart
@@ -1,12 +1,12 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
- List data;
- Meta meta;
+ List? 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 json) => OfferPurchaseHistoryResponseModel(
- data: List.from(json["data"].map((x) => Datum.fromJson(x))),
- meta: Meta.fromJson(json["meta"]),
+ data: json["data"] == null ? [] : List.from(json["data"]!.map((x) => Datum.fromJson(x))),
+ meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]),
);
Map toJson() => {
- "data": List.from(data.map((x) => x.toJson())),
- "meta": meta.toJson(),
+ "data": data == null ? [] : List.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 branches;
+ String? qrCode;
+ String? qrCodeUrl;
+ String? pdfUrl;
+ dynamic validTo;
+ UsageValidity? usageValidity;
+ Offer? offer;
+ String? branchApplicability;
+ List? 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 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.from(json["branches"].map((x) => Branch.fromJson(x))),
+ branches: json["branches"] == null ? [] : List.from(json["branches"]!.map((x) => Branch.fromJson(x))),
);
Map 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.from(branches.map((x) => x.toJson())),
+ "branches": branches == null ? [] : List.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 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));
@@ -226,4 +226,4 @@ class Meta {
"per_page": perPage,
"pages": pages,
};
-}
\ No newline at end of file
+}
diff --git a/lib/features/offers_and_discounts/offers_and_discounts_repo.dart b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart
index fb86abe1..56e1410f 100644
--- a/lib/features/offers_and_discounts/offers_and_discounts_repo.dart
+++ b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart
@@ -34,14 +34,15 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
- Future>> authenticatePatient(String patientId, String mobileNumber) async {
+ Future>> authenticatePatient(String nationalID, String mobileNumber) async {
Map headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
Map body = {
- 'patient_id': patientId,
+ 'national_id': nationalID,
+ // 'patient_id': patientId,
'mobile_number': mobileNumber,
};
diff --git a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart
index 29cf4339..e7cab417 100644
--- a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart
+++ b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart
@@ -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 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 _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 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 getAllImageUrls(OffersAndDiscountsDetailedResponseModel offer) {
+ List 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 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 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 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 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