Offers And Discounts Apis

offers_discounts
Aamir Muhammad 1 month ago
parent 369a566006
commit b02831834f

@ -302,9 +302,12 @@ class ApiConsts {
// POMS Offers and Discounts API // POMS Offers and Discounts API
static final pomsBaseUrl = 'https://poms-uat.odoo.com'; static final pomsBaseUrl = 'https://poms-uat.odoo.com';
static final pomsAuthPatient = '$pomsBaseUrl/api/v1/mobile/auth/patient';
static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers'; static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers'; static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetOffersAndDiscountPurchasedHistory = '$pomsBaseUrl/api/v1/mobile/my/offers'; static final pomsGetOffersAndDiscountPurchasedHistory = '$pomsBaseUrl/api/v1/mobile/my/offers';
static String pomsPurchaseOffer(int offerId) => '$pomsBaseUrl/api/v1/mobile/offers/$offerId/purchase';
static String pomsPaymentResult(int orderId) => '$pomsBaseUrl/api/v1/mobile/orders/$orderId/payment-result';
// ************ static values for Api **************** // ************ static values for Api ****************
static final double appVersionID = 21.3; static final double appVersionID = 21.3;

@ -56,7 +56,7 @@ class AppState {
void setIsPayfortHostedPageEnabled(bool value) { void setIsPayfortHostedPageEnabled(bool value) {
isPayfortHostedPageEnabled = value; isPayfortHostedPageEnabled = value;
// isPayfortHostedPageEnabled = true; // isPayfortHostedPageEnabled = false;
} }
int? _superUserID; int? _superUserID;
@ -170,6 +170,31 @@ class AppState {
set setIsAuthenticated(v) => isAuthenticated = v; set setIsAuthenticated(v) => isAuthenticated = v;
// POMS Authentication for Offers and Discounts
String _pomsAuthToken = "";
DateTime? _pomsAuthTokenExpiresAt;
String get getPomsAuthToken => _pomsAuthToken;
DateTime? get getPomsAuthTokenExpiresAt => _pomsAuthTokenExpiresAt;
void setPomsAuthToken(String token, DateTime expiresAt) {
_pomsAuthToken = token;
_pomsAuthTokenExpiresAt = expiresAt;
}
void clearPomsAuthToken() {
_pomsAuthToken = "";
_pomsAuthTokenExpiresAt = null;
}
bool isPomsAuthTokenValid() {
if (_pomsAuthToken.isEmpty || _pomsAuthTokenExpiresAt == null) {
return false;
}
return DateTime.now().isBefore(_pomsAuthTokenExpiresAt!);
}
String deviceTypeID = ""; String deviceTypeID = "";

@ -336,7 +336,7 @@ extension ServiceTypeEnumExt on ServiceTypeEnum {
case ServiceTypeEnum.liveCareAppointment: case ServiceTypeEnum.liveCareAppointment:
return 4; return 4;
case ServiceTypeEnum.offerPayment: case ServiceTypeEnum.offerPayment:
return 5; return 18;
} }
} }
} }

@ -0,0 +1,30 @@
import 'dart:convert';
class OfferPurchaseRequestModel {
String identificationType;
String idValue;
String mobileNumber;
OfferPurchaseRequestModel({
required this.identificationType,
required this.idValue,
required this.mobileNumber,
});
factory OfferPurchaseRequestModel.fromRawJson(String str) => OfferPurchaseRequestModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseRequestModel.fromJson(Map<String, dynamic> json) => OfferPurchaseRequestModel(
identificationType: json["identification_type"],
idValue: json["id_value"],
mobileNumber: json["mobile_number"],
);
Map<String, dynamic> toJson() => {
"identification_type": identificationType,
"id_value": idValue,
"mobile_number": mobileNumber,
};
}

@ -0,0 +1,41 @@
import 'dart:convert';
class OfferPurchaseResponseModel {
int orderId;
String orderRef;
String amount;
String currency;
bool paymentRequired;
dynamic paymentUrl;
OfferPurchaseResponseModel({
required this.orderId,
required this.orderRef,
required this.amount,
required this.currency,
required this.paymentRequired,
required this.paymentUrl,
});
factory OfferPurchaseResponseModel.fromRawJson(String str) => OfferPurchaseResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseResponseModel.fromJson(Map<String, dynamic> json) => OfferPurchaseResponseModel(
orderId: json["order_id"],
orderRef: json["order_ref"],
amount: json["amount"],
currency: json["currency"],
paymentRequired: json["payment_required"],
paymentUrl: json["payment_url"],
);
Map<String, dynamic> toJson() => {
"order_id": orderId,
"order_ref": orderRef,
"amount": amount,
"currency": currency,
"payment_required": paymentRequired,
"payment_url": paymentUrl,
};
}

@ -0,0 +1,229 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
List<Datum> data;
Meta meta;
OfferPurchaseHistoryResponseModel({
required this.data,
required this.meta,
});
factory OfferPurchaseHistoryResponseModel.fromRawJson(String str) => OfferPurchaseHistoryResponseModel.fromJson(json.decode(str));
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"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
};
}
class Datum {
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;
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,
});
factory Datum.fromRawJson(String str) => Datum.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
voucherCode: json["voucher_code"],
state: json["state"],
purchaseDate: 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"]),
branchApplicability: json["branch_applicability"],
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"voucher_code": voucherCode,
"state": state,
"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(),
"branch_applicability": branchApplicability,
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
};
}
class Branch {
String code;
String name;
String nameAr;
Branch({
required this.code,
required this.name,
required this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Branch.fromJson(Map<String, dynamic> json) => Branch(
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Offer {
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,
});
factory Offer.fromRawJson(String str) => Offer.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Offer.fromJson(Map<String, dynamic> json) => Offer(
id: json["id"],
name: json["name"],
nameAr: json["name_ar"],
branchApplicability: json["branch_applicability"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
"id": id,
"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(),
};
}
class UsageValidity {
String type;
dynamic days;
String rule;
String ruleAr;
DateTime redeemBy;
String redeemByDisplay;
UsageValidity({
required this.type,
required this.days,
required this.rule,
required this.ruleAr,
required this.redeemBy,
required 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: json["type"],
days: json["days"],
rule: json["rule"],
ruleAr: json["rule_ar"],
redeemBy: DateTime.parse(json["redeem_by"]),
redeemByDisplay: json["redeem_by_display"],
);
Map<String, dynamic> toJson() => {
"type": type,
"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_display": redeemByDisplay,
};
}
class Meta {
int total;
int page;
int perPage;
int pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
total: json["total"],
page: json["page"],
perPage: json["per_page"],
pages: json["pages"],
);
Map<String, dynamic> toJson() => {
"total": total,
"page": page,
"per_page": perPage,
"pages": pages,
};
}

@ -0,0 +1,229 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
List<Datum> data;
Meta meta;
OfferPurchaseHistoryResponseModel({
required this.data,
required this.meta,
});
factory OfferPurchaseHistoryResponseModel.fromRawJson(String str) => OfferPurchaseHistoryResponseModel.fromJson(json.decode(str));
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"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
};
}
class Datum {
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;
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,
});
factory Datum.fromRawJson(String str) => Datum.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
voucherCode: json["voucher_code"],
state: json["state"],
purchaseDate: 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"]),
branchApplicability: json["branch_applicability"],
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"voucher_code": voucherCode,
"state": state,
"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(),
"branch_applicability": branchApplicability,
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
};
}
class Branch {
String code;
String name;
String nameAr;
Branch({
required this.code,
required this.name,
required this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Branch.fromJson(Map<String, dynamic> json) => Branch(
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Offer {
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,
});
factory Offer.fromRawJson(String str) => Offer.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Offer.fromJson(Map<String, dynamic> json) => Offer(
id: json["id"],
name: json["name"],
nameAr: json["name_ar"],
branchApplicability: json["branch_applicability"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
"id": id,
"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(),
};
}
class UsageValidity {
String type;
dynamic days;
String rule;
String ruleAr;
DateTime redeemBy;
String redeemByDisplay;
UsageValidity({
required this.type,
required this.days,
required this.rule,
required this.ruleAr,
required this.redeemBy,
required 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: json["type"],
days: json["days"],
rule: json["rule"],
ruleAr: json["rule_ar"],
redeemBy: DateTime.parse(json["redeem_by"]),
redeemByDisplay: json["redeem_by_display"],
);
Map<String, dynamic> toJson() => {
"type": type,
"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_display": redeemByDisplay,
};
}
class Meta {
int total;
int page;
int perPage;
int pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
total: json["total"],
page: json["page"],
perPage: json["per_page"],
pages: json["pages"],
);
Map<String, dynamic> toJson() => {
"total": total,
"page": page,
"per_page": perPage,
"pages": pages,
};
}

@ -0,0 +1,34 @@
import 'dart:convert';
class PaymentResultRequestModel {
String status;
String paymentRef;
String amount;
String currency;
PaymentResultRequestModel({
required this.status,
required this.paymentRef,
required this.amount,
required this.currency,
});
factory PaymentResultRequestModel.fromRawJson(String str) => PaymentResultRequestModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PaymentResultRequestModel.fromJson(Map<String, dynamic> json) => PaymentResultRequestModel(
status: json["status"],
paymentRef: json["payment_ref"],
amount: json["amount"],
currency: json["currency"],
);
Map<String, dynamic> toJson() => {
"status": status,
"payment_ref": paymentRef,
"amount": amount,
"currency": currency,
};
}

@ -0,0 +1,22 @@
import 'dart:convert';
class PaymentResultResponseModel {
String status;
PaymentResultResponseModel({
required this.status,
});
factory PaymentResultResponseModel.fromRawJson(String str) => PaymentResultResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PaymentResultResponseModel.fromJson(Map<String, dynamic> json) => PaymentResultResponseModel(
status: json["status"],
);
Map<String, dynamic> toJson() => {
"status": status,
};
}

@ -0,0 +1,92 @@
import 'dart:convert';
class PomsAuthTokenModel {
String token;
DateTime expiresAt;
Partner partner;
PortalAccount portalAccount;
PomsAuthTokenModel({
required this.token,
required this.expiresAt,
required this.partner,
required this.portalAccount,
});
factory PomsAuthTokenModel.fromRawJson(String str) => PomsAuthTokenModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PomsAuthTokenModel.fromJson(Map<String, dynamic> json) => PomsAuthTokenModel(
token: json["token"],
expiresAt: DateTime.parse(json["expires_at"]),
partner: Partner.fromJson(json["partner"]),
portalAccount: PortalAccount.fromJson(json["portal_account"]),
);
Map<String, dynamic> toJson() => {
"token": token,
"expires_at": expiresAt.toIso8601String(),
"partner": partner.toJson(),
"portal_account": portalAccount.toJson(),
};
// Helper method to check if token is expired
bool isExpired() {
return DateTime.now().isAfter(expiresAt);
}
// Helper method to check if token is valid
bool isValid() {
return !isExpired() && token.isNotEmpty;
}
}
class Partner {
int id;
String name;
Partner({
required this.id,
required this.name,
});
factory Partner.fromRawJson(String str) => Partner.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Partner.fromJson(Map<String, dynamic> json) => Partner(
id: json["id"],
name: json["name"],
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
};
}
class PortalAccount {
bool created;
String login;
PortalAccount({
required this.created,
required this.login,
});
factory PortalAccount.fromRawJson(String str) => PortalAccount.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PortalAccount.fromJson(Map<String, dynamic> json) => PortalAccount(
created: json["created"],
login: json["login"],
);
Map<String, dynamic> toJson() => {
"created": created,
"login": login,
};
}

@ -5,14 +5,26 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.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/models/offers_and_discounts_detailed_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/poms_auth_token_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_purchases_history_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class OffersAndDiscountsRepo { abstract class OffersAndDiscountsRepo {
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String patientId, String mobileNumber);
Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts(); Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts();
Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId); Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId);
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory(); Future<Either<Failure, GenericApiModel<OfferPurchaseHistoryResponseModel>>> getOffersAndDiscountsHistory(String authToken);
Future<Either<Failure, GenericApiModel<OfferPurchaseResponseModel>>> purchaseOffer(int offerId, OfferPurchaseRequestModel request, String authToken);
Future<Either<Failure, GenericApiModel<PaymentResultResponseModel>>> sendPaymentResult(int orderId, PaymentResultRequestModel request, String authToken);
} }
class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo { class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
@ -21,6 +33,57 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient}); OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String patientId, 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,
'mobile_number': mobileNumber,
};
try {
GenericApiModel<PomsAuthTokenModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.pomsAuthPatient,
body: body,
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final authResponse = PomsAuthTokenModel.fromJson(response);
apiResponse = GenericApiModel<PomsAuthTokenModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: authResponse,
);
} catch (e) {
loggerService.logError("Error parsing POMS auth response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in authenticatePatient: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override @override
Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts() async { Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts() async {
Map<String, String> headers = { Map<String, String> headers = {
@ -109,42 +172,142 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
} }
@override @override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory() async { @override
Map<String, dynamic> mapDevice = {}; Future<Either<Failure, GenericApiModel<OfferPurchaseHistoryResponseModel>>> getOffersAndDiscountsHistory(String authToken) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
'Authorization': 'Bearer $authToken',
};
try { try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse; GenericApiModel<OfferPurchaseHistoryResponseModel>? apiResponse;
Failure? failure; Failure? failure;
await apiClient.post(
await apiClient.get(
ApiConsts.pomsGetOffersAndDiscountPurchasedHistory, ApiConsts.pomsGetOffersAndDiscountPurchasedHistory,
body: mapDevice, apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final historyResponse = OfferPurchaseHistoryResponseModel.fromJson(response);
apiResponse = GenericApiModel<OfferPurchaseHistoryResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: historyResponse,
);
} catch (e) {
loggerService.logError("Error parsing offer purchase history response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in getOffersAndDiscountsHistory: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<OfferPurchaseResponseModel>>> purchaseOffer(
int offerId,
OfferPurchaseRequestModel request,
String authToken,
) async {
Map<String, String> headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey, 'Authorization': 'Bearer $authToken'};
try {
GenericApiModel<OfferPurchaseResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.pomsPurchaseOffer(offerId),
body: request.toJson(),
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) { onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType; failure = ServerFailure(error);
}, },
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
final list = response['List_OffersAndDiscountsHistory']; final purchaseResponse = OfferPurchaseResponseModel.fromJson(response);
if (list == null || list.isEmpty) {
throw Exception("offers and discounts history list is empty"); apiResponse = GenericApiModel<OfferPurchaseResponseModel>(
} messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: purchaseResponse,
);
} catch (e) {
loggerService.logError("Error parsing offer purchase response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in purchaseOffer: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<PaymentResultResponseModel>>> sendPaymentResult(
int orderId,
PaymentResultRequestModel request,
String authToken,
) async {
Map<String, String> headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey, 'Authorization': 'Bearer $authToken'};
final historyOffers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>(); try {
GenericApiModel<PaymentResultResponseModel>? apiResponse;
Failure? failure;
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>( await apiClient.post(
messageStatus: messageStatus, ApiConsts.pomsPaymentResult(orderId),
body: request.toJson(),
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final paymentResultResponse = PaymentResultResponseModel.fromJson(response);
apiResponse = GenericApiModel<PaymentResultResponseModel>(
messageStatus: 1,
statusCode: statusCode, statusCode: statusCode,
errorMessage: null, errorMessage: null,
data: historyOffers, data: paymentResultResponse,
); );
} catch (e) { } catch (e) {
loggerService.logError("Error parsing payment result response: $e");
failure = DataParsingFailure(e.toString()); failure = DataParsingFailure(e.toString());
} }
}, },
); );
if (failure != null) return Left(failure!); if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error")); if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!); return Right(apiResponse!);
} catch (e) { } catch (e) {
loggerService.logError("Exception in sendPaymentResult: $e");
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }

@ -1,6 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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'; import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart' hide Category; import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart' hide Category;
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/poms_auth_token_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_response_model.dart';
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/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -8,6 +16,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false; bool isOffersLoading = false;
bool isHistoryLoading = false; bool isHistoryLoading = false;
bool isOfferDetailsLoading = false; bool isOfferDetailsLoading = false;
bool isAuthenticating = false;
bool isPurchasing = false;
List<int> selectedCategories = []; // Empty means 'All Offers' List<int> selectedCategories = []; // Empty means 'All Offers'
String searchQuery = ''; String searchQuery = '';
@ -15,10 +25,12 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
ErrorHandlerService errorHandlerService; ErrorHandlerService errorHandlerService;
List<OfferData> offersList = []; List<OfferData> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = []; history_model.OfferPurchaseHistoryResponseModel? purchaseHistory;
OffersAndDiscountsDetailedResponseModel? offerDetails; OffersAndDiscountsDetailedResponseModel? offerDetails;
OffersAndDiscountsDetailedResponseModel? selectedOffer; OffersAndDiscountsDetailedResponseModel? selectedOffer;
Meta? meta; Meta? meta;
PomsAuthTokenModel? pomsAuthToken;
OfferPurchaseResponseModel? lastPurchaseResponse;
OffersAndDiscountsViewModel({ OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo, required this.offersAndDiscountsRepo,
@ -28,11 +40,27 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
// For loading state compatibility with existing code // For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading; bool get isRegionListLoading => isOffersLoading;
initOffersAndDiscounts() { initOffersAndDiscounts({bool forceRefresh = false}) {
// Only clear data if force refresh, otherwise keep existing data
if (forceRefresh) {
offersList.clear();
}
// Only show loading if we don't have data yet
if (offersList.isEmpty) {
isOffersLoading = true;
notifyListeners();
}
getOffersAndDiscounts();
}
// Method to force refresh the offers list
Future<void> refreshOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
offersList.clear(); offersList.clear();
isOffersLoading = true; isOffersLoading = true;
notifyListeners(); notifyListeners();
getOffersAndDiscounts(); await getOffersAndDiscounts(onSuccess: onSuccess, onError: onError);
} }
setIsOffersLoading(bool val) { setIsOffersLoading(bool val) {
@ -45,6 +73,204 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
setIsAuthenticating(bool val) {
isAuthenticating = val;
notifyListeners();
}
/// Authenticate patient before making a purchase
Future<bool> authenticatePatient({Function(PomsAuthTokenModel)? onSuccess, Function(String)? onError}) async {
final appState = getIt.get<AppState>();
final authenticatedUser = appState.getAuthenticatedUser();
if (authenticatedUser == null) {
if (onError != null) {
onError("User not authenticated");
}
return false;
}
// Check if token is already valid
if (appState.isPomsAuthTokenValid()) {
if (onSuccess != null && pomsAuthToken != null) {
onSuccess(pomsAuthToken!);
}
return true;
}
isAuthenticating = true;
notifyListeners();
final result = await offersAndDiscountsRepo.authenticatePatient(
authenticatedUser.patientId.toString(),
// "4774103",
// "2001273",
authenticatedUser.mobileNumber ?? "",
);
result.fold(
(failure) async {
isAuthenticating = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isAuthenticating = false;
if (response.data != null) {
pomsAuthToken = response.data;
// Store token in AppState
appState.setPomsAuthToken(response.data!.token, response.data!.expiresAt);
}
notifyListeners();
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return pomsAuthToken != null && pomsAuthToken!.isValid();
}
/// Ensure patient is authenticated, re-authenticate if token is invalid or expired
/// This is a convenience method for Buy Now buttons and other actions requiring auth
Future<bool> ensureAuthenticated({
Function()? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Check if token is already valid
if (appState.isPomsAuthTokenValid()) {
if (onSuccess != null) {
onSuccess();
}
return true;
}
// Token is invalid or expired, re-authenticate
final isAuthenticated = await authenticatePatient(
onSuccess: (authToken) {
print("POMS Authentication successful: ${authToken.token}");
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
print("POMS Authentication failed: $error");
if (onError != null) {
onError(error);
}
},
);
return isAuthenticated;
}
/// Purchase an offer after payment is successful
Future<bool> purchaseOffer({
required int offerId,
required String identificationType,
required String idValue,
required String mobileNumber,
Function(OfferPurchaseResponseModel)? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return false;
}
isPurchasing = true;
notifyListeners();
final request = OfferPurchaseRequestModel(identificationType: identificationType, idValue: idValue, mobileNumber: mobileNumber);
final result = await offersAndDiscountsRepo.purchaseOffer(offerId, request, appState.getPomsAuthToken);
result.fold(
(failure) async {
isPurchasing = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isPurchasing = false;
if (response.data != null) {
lastPurchaseResponse = response.data;
print(lastPurchaseResponse!.toRawJson());
}
notifyListeners();
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return lastPurchaseResponse != null;
}
/// Send payment result to backend after successful payment
Future<bool> sendPaymentResult({
required int orderId,
required String status,
required String paymentRef,
required String amount,
required String currency,
Function(PaymentResultResponseModel)? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return false;
}
final request = PaymentResultRequestModel(
status: status,
paymentRef: paymentRef,
amount: amount,
currency: currency,
);
final result = await offersAndDiscountsRepo.sendPaymentResult(orderId, request, appState.getPomsAuthToken);
bool success = false;
result.fold(
(failure) async {
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
if (response.data != null) {
success = response.data!.status.toLowerCase() == "ok";
}
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return success;
}
setSelectedCategories(List<int> categoryIds) { setSelectedCategories(List<int> categoryIds) {
selectedCategories = categoryIds; selectedCategories = categoryIds;
notifyListeners(); notifyListeners();
@ -75,8 +301,13 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
} }
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isOffersLoading = true; // Only show loading if we don't have data yet
notifyListeners(); final bool hasExistingData = offersList.isNotEmpty;
if (!hasExistingData) {
isOffersLoading = true;
notifyListeners();
}
final result = await offersAndDiscountsRepo.getOffersAndDiscounts(); final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
@ -84,7 +315,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
(failure) async { (failure) async {
isOffersLoading = false; isOffersLoading = false;
notifyListeners(); notifyListeners();
// Keep dummy data if API fails - don't clear the list // Keep existing data if API fails
await errorHandlerService.handleError(failure: failure); await errorHandlerService.handleError(failure: failure);
if (onError != null) { if (onError != null) {
onError(failure.toString()); onError(failure.toString());
@ -93,10 +324,22 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
(response) { (response) {
isOffersLoading = false; isOffersLoading = false;
if (response.data != null) { if (response.data != null) {
offersList = response.data!.data; // If we already have data, append new data instead of replacing
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();
// Append only new items
if (newItems.isNotEmpty) {
offersList.addAll(newItems);
}
} else {
// First load - replace the list
offersList = response.data!.data;
}
meta = response.data!.meta; meta = response.data!.meta;
} }
// If API returns empty or null, keep the dummy data
notifyListeners(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
onSuccess(response); onSuccess(response);
@ -134,27 +377,37 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
} }
Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
isHistoryLoading = false;
notifyListeners();
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return;
}
isHistoryLoading = true; isHistoryLoading = true;
notifyListeners(); notifyListeners();
final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory(); final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory(appState.getPomsAuthToken);
result.fold( result.fold(
(failure) async { (failure) async {
isHistoryLoading = false; isHistoryLoading = false;
notifyListeners(); notifyListeners();
// Keep dummy data if API fails - don't clear the list await errorHandlerService.handleError(failure: failure);
// await errorHandlerService.handleError(failure: failure);
if (onError != null) { if (onError != null) {
onError(failure.toString()); onError(failure.toString());
} }
}, },
(response) { (response) {
isHistoryLoading = false; isHistoryLoading = false;
if (response.data != null && response.data!.isNotEmpty) { if (response.data != null) {
historyList = response.data!; purchaseHistory = response.data;
} }
// If API returns empty or null, keep the dummy data
notifyListeners(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
onSuccess(response); onSuccess(response);
@ -163,6 +416,10 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
); );
} }
List<history_model.Datum> get historyList {
return purchaseHistory?.data ?? [];
}
List<OfferData> get filteredOffers { List<OfferData> get filteredOffers {
List<OfferData> filtered = offersList; List<OfferData> filtered = offersList;
@ -185,14 +442,16 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
return filtered; return filtered;
} }
List<OffersAndDiscountsResponseModel> get filteredHistory { List<history_model.Datum> get filteredHistory {
List<OffersAndDiscountsResponseModel> filtered = historyList; List<history_model.Datum> filtered = historyList;
// Filter by search query // Filter by search query
if (searchQuery.isNotEmpty) { if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) { filtered = filtered.where((history) {
// Assuming history list uses the old structure - update when API is available final offerNameMatch = history.offer.name.toLowerCase().contains(searchQuery.toLowerCase());
return true; // Placeholder for now final offerNameArMatch = history.offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
final voucherCodeMatch = history.voucherCode.toLowerCase().contains(searchQuery.toLowerCase());
return offerNameMatch || offerNameArMatch || voucherCodeMatch;
}).toList(); }).toList();
} }

@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
@ -99,6 +100,7 @@ class _LandingPageState extends State<LandingPage> {
late EmergencyServicesViewModel emergencyServicesViewModel; late EmergencyServicesViewModel emergencyServicesViewModel;
late TodoSectionViewModel todoSectionViewModel; late TodoSectionViewModel todoSectionViewModel;
late NotificationsViewModel notificationsViewModel; late NotificationsViewModel notificationsViewModel;
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
final SwiperController _controller = SwiperController(); final SwiperController _controller = SwiperController();
@ -159,6 +161,7 @@ class _LandingPageState extends State<LandingPage> {
// myAppointmentsViewModel.getPatientAppointmentQueueDetails(); // myAppointmentsViewModel.getPatientAppointmentQueueDetails();
notificationsViewModel.initNotificationsViewModel(); notificationsViewModel.initNotificationsViewModel();
insuranceViewModel.initInsuranceProvider(); insuranceViewModel.initInsuranceProvider();
offersAndDiscountsViewModel.initOffersAndDiscounts();
if (!appState.isRatedVisible) { if (!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment( appointmentRatingViewModel.getLastRatingAppointment(
@ -197,6 +200,7 @@ class _LandingPageState extends State<LandingPage> {
emergencyServicesViewModel = Provider.of<EmergencyServicesViewModel>(context, listen: false); emergencyServicesViewModel = Provider.of<EmergencyServicesViewModel>(context, listen: false);
todoSectionViewModel = Provider.of<TodoSectionViewModel>(context, listen: false); todoSectionViewModel = Provider.of<TodoSectionViewModel>(context, listen: false);
notificationsViewModel = Provider.of<NotificationsViewModel>(context, listen: false); notificationsViewModel = Provider.of<NotificationsViewModel>(context, listen: false);
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
return PopScope( return PopScope(
canPop: false, canPop: false,
@ -244,7 +248,6 @@ class _LandingPageState extends State<LandingPage> {
// } // }
// }, // },
// ); // );
} }
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
@ -448,7 +451,8 @@ class _LandingPageState extends State<LandingPage> {
: SizedBox.shrink(), : SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left // Offers And Discounts Carousel - Auto-scrolling from right to left
appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel //appState.isAuthenticated &&
appState.isEnabledOffersAndDiscountsCarousel && offersAndDiscountsViewModel.offersList.isNotEmpty
? Column( ? Column(
children: [ children: [
SizedBox(height: 12.h), SizedBox(height: 12.h),
@ -536,9 +540,7 @@ class _LandingPageState extends State<LandingPage> {
height: isTablet ? 290.h : 255.h, height: isTablet ? 290.h : 255.h,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3,
? myAppointmentsVM.patientAppointmentsHistoryList.length
: 3,
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h), padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {

@ -8,12 +8,15 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.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/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:easy_localization/easy_localization.dart';
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget { class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
final int offerId; final int offerId;
@ -318,7 +321,28 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
text: 'Buy Now', text: 'Buy Now',
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { onPressed: () async {
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await vm.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,
);
},
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page // Set the selected offer and navigate to payment page
if (vm.offerDetails != null) { if (vm.offerDetails != null) {
vm.setSelectedOffer(vm.offerDetails!); vm.setSelectedOffer(vm.offerDetails!);

@ -0,0 +1,290 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/models/offers_purchases_history_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.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';
class OffersAndDiscountsSuccessQrPage extends StatefulWidget {
final Datum purchaseData;
const OffersAndDiscountsSuccessQrPage({super.key, required this.purchaseData});
@override
State<OffersAndDiscountsSuccessQrPage> createState() => _OffersAndDiscountsSuccessQrPageState();
}
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();
}
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return AppColors.successColor;
case 'redeemed':
case 'used':
return AppColors.warningColor;
case 'expired':
return AppColors.errorColor;
default:
return AppColors.greyTextColor;
}
}
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);
default:
return status.toUpperCase();
}
}
void _copyVoucherCode() {
Clipboard.setData(ClipboardData(text: widget.purchaseData.voucherCode));
Utils.showToast("Voucher code copied to clipboard");
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.paymentSuccessful.tr(context: context),
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,
),
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)),
),
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),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code,
size: 80.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
"QR Code".toText12(color: AppColors.greyTextColor),
],
),
),
),
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: 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),
// Order Details Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.orderDetails.tr(context: context).toText18(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),
borderRadius: BorderRadius.circular(12.h),
),
child: _getStatusText(widget.purchaseData.state).toText12(
isBold: true,
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),
],
],
).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,
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),
),
),
],
);
}
}

@ -22,7 +22,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -47,7 +47,6 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
late AppState appState; late AppState appState;
late PayTabsViewModel paytabsViewModel; late PayTabsViewModel paytabsViewModel;
MyInAppBrowser? browser; MyInAppBrowser? browser;
String selectedPaymentMethod = ""; String selectedPaymentMethod = "";
@ -159,24 +158,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
).paddingSymmetrical(16.h, 16.h), ).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async { ).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA"; selectedPaymentMethod = "MADA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) { if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration( paytabsViewModel.setPaymentConfiguration(
"Offer Payment", "Offer Payment",
selectedOffer.listPrice, selectedOffer.vatExclusivePrice,
); );
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr()); await completePaymentResult(paymentStatus: "success");
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) { }, onError: (err) {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
@ -188,35 +189,15 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
}); });
} else { } else {
if (appState.isPayfortHostedPageEnabled) { if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context)); await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(), amount: selectedOffer.vatExclusivePrice.toString(),
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await completePaymentResult(paymentStatus: "success");
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, },
onFailed: (err) { onFailed: (err) {
print("Payment Failed Data: ${err.toString()}"); print("Payment Failed Data: ${err.toString()}");
@ -230,7 +211,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
}, },
); );
} else { } else {
openPaymentURL("mada", selectedOffer.listPrice); LoaderBottomSheet.hideLoader();
openPaymentURL("mada", selectedOffer.vatExclusivePrice);
} }
} }
}), }),
@ -274,24 +256,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
).paddingSymmetrical(16.h, 16.h), ).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async { ).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA"; selectedPaymentMethod = "VISA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) { if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration( paytabsViewModel.setPaymentConfiguration(
"Offer Payment", "Offer Payment",
selectedOffer.listPrice, selectedOffer.vatExclusivePrice,
); );
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr()); await completePaymentResult(paymentStatus: "success");
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) { }, onError: (err) {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
@ -303,35 +287,15 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
}); });
} else { } else {
if (appState.isPayfortHostedPageEnabled) { if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context)); await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(), amount: selectedOffer.vatExclusivePrice.toString(),
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); await completePaymentResult(paymentStatus: "success");
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, },
onFailed: (err) { onFailed: (err) {
print("Payment Failed Data: ${err.toString()}"); print("Payment Failed Data: ${err.toString()}");
@ -345,7 +309,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
}, },
); );
} else { } else {
openPaymentURL("visa", selectedOffer.listPrice); LoaderBottomSheet.hideLoader();
openPaymentURL("visa", selectedOffer.vatExclusivePrice);
} }
} }
}), }),
@ -371,8 +336,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.amountBeforeTax.tr().toText14(isBold: true), LocaleKeys.amountBeforeTax.tr().toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.vatExclusivePrice.toString().toText16(isBold: true), AppColors.blackColor, 13, Utils.getPaymentAmountWithSymbol(selectedOffer.vatExclusivePrice.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: selectedOffer.currency == "SAR"),
isSaudiCurrency: selectedOffer.currency == "SAR"),
], ],
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
Row( Row(
@ -389,24 +353,30 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
"".toText14(isBold: true), "".toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17, Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: selectedOffer.currency == "SAR"),
isSaudiCurrency: selectedOffer.currency == "SAR"),
], ],
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
Platform.isIOS Platform.isIOS
? Utils.buildSvgWithAssets( ? Utils.buildSvgWithAssets(icon: AppAssets.apple_pay_button, width: 200.h, height: 80.h, fit: BoxFit.contain, applyThemeColor: false)
icon: AppAssets.apple_pay_button, .paddingSymmetrical(24.h, 0.h)
width: 200.h, .onPress(() async {
height: 80.h, if (Utils.havePrivilege(103)) {
fit: BoxFit.contain, await startApplePay(selectedOffer.vatExclusivePrice);
applyThemeColor: false } else {
).paddingSymmetrical(24.h, 0.h).onPress(() { // Create order first for non-privileged Apple Pay
if (Utils.havePrivilege(103)) { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
startApplePay(selectedOffer.listPrice); String? orderRef = await createOrder();
} else {
openPaymentURL("ApplePay", selectedOffer.listPrice); if (orderRef == null) {
} // Error already shown in createOrder
}) return;
}
transID = orderRef;
LoaderBottomSheet.hideLoader();
openPaymentURL("ApplePay", selectedOffer.vatExclusivePrice);
}
})
: SizedBox(height: 12.h), : SizedBox(height: 12.h),
SizedBox(height: 12.h), SizedBox(height: 12.h),
], ],
@ -470,19 +440,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (apiResponse.data["status"].toString().toLowerCase() == "success") { if (apiResponse.data["status"].toString().toLowerCase() == "success") {
tamaraOrderID = apiResponse.data["tamara_order_id"].toString(); tamaraOrderID = apiResponse.data["tamara_order_id"].toString();
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
// TODO: Call API to complete offer purchase await completePaymentResult(paymentStatus: "success");
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else { } else {
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
@ -509,17 +468,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
onSuccess: (apiResponse) async { onSuccess: (apiResponse) async {
debugPrint(apiResponse.data.toString()); debugPrint(apiResponse.data.toString());
if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") {
// TODO: Call API to complete offer purchase // Order already created, just show success
LoaderBottomSheet.hideLoader(); await completePaymentResult(paymentStatus: "success");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else { } else {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
@ -540,29 +490,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (selectedOffer == null) return; if (selectedOffer == null) return;
transID = Utils.getAppointmentTransID( // Use existing transID (order_ref from createOrder)
selectedOffer.id, // transID should already be set before calling this method
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
browser?.openPaymentBrowser( browser?.openPaymentBrowser(
amount, amount,
"Offer Payment", "Offer Payment",
transID, transID,
"12", "0",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
selectedPaymentMethod, selectedPaymentMethod,
appState.getAuthenticatedUser()!.patientType.toString(), appState.getAuthenticatedUser()!.patientType.toString(),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!, appState.getAuthenticatedUser()!,
browser!, browser!,
false, false,
"4", "18",
selectedOffer.id.toString(), "0",
context, context,
"3"); );
} }
startApplePay(double amount) async { startApplePay(double amount) async {
@ -574,18 +521,19 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
return; return;
} }
transID = Utils.getAppointmentTransID( // Create order first to get order_ref
selectedOffer.id, String? orderRef = await createOrder();
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch, if (orderRef == null) {
); // Error already shown in createOrder
return;
}
transID = orderRef;
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
await payfortViewModel.getPayfortConfigurations( await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 2);
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 2);
applePayInsertRequest.clientRequestID = transID; applePayInsertRequest.clientRequestID = transID;
applePayInsertRequest.clinicID = selectedOffer.id; applePayInsertRequest.clinicID = selectedOffer.id;
@ -634,28 +582,11 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
selectedPaymentMethod = "VISA"; selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Offer Payment", amount); paytabsViewModel.setPaymentConfiguration("Offer Payment", amount);
paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async { paytabsViewModel.startApplePayPayment(
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr()); onSuccess: (PaytabsTransactionResponseModel transactionData) async {
// TODO: Call API to complete offer purchase await completePaymentResult(paymentStatus: "success");
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
}, },
isFullScreen: false, onError: (err) {});
isCloseButtonVisible: true,
);
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else { } else {
payfortViewModel.paymentWithApplePay( payfortViewModel.paymentWithApplePay(
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
@ -688,4 +619,140 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
} }
}); });
} }
/// Create order before payment to get order reference
Future<String?> createOrder() async {
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
final authenticatedUser = appState.getAuthenticatedUser();
if (selectedOffer == null || authenticatedUser == null) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unable to create order. Missing offer or user information."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
return null;
}
// Determine identification type based on patient identification type
String identificationType = "national_id"; // Default
// if (authenticatedUser.patientIdentificationType != null) {
// switch (authenticatedUser.patientIdentificationType) {
// case 1:
// identificationType = "national_id";
// break;
// case 2:
// identificationType = "iqama";
// break;
// case 3:
// identificationType = "passport";
// break;
// case 4:
// identificationType = "gcc_id";
// break;
// default:
// identificationType = "national_id";
// }
// }
String? orderRef;
await offersAndDiscountsViewModel.purchaseOffer(
offerId: selectedOffer.id,
identificationType: identificationType,
// idValue: "2011594252",
// idValue: "4774103",
idValue: authenticatedUser.patientIdentificationNo ?? "",
mobileNumber: authenticatedUser.mobileNumber ?? "",
onSuccess: (purchaseResponse) {
// Get the order_ref from the purchase response
orderRef = purchaseResponse.orderRef;
},
onError: (error) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Failed to create order. Please try again."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
return orderRef;
}
/// Show payment success message
Future<void> completePaymentResult({required String paymentStatus}) async {
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
final lastPurchaseResponse = offersAndDiscountsViewModel.lastPurchaseResponse;
if (selectedOffer == null || lastPurchaseResponse == null) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unable to complete payment. Missing order information."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
return;
}
// Call the payment result API
await offersAndDiscountsViewModel.sendPaymentResult(
orderId: lastPurchaseResponse.orderId,
status: paymentStatus,
paymentRef: transID,
amount: selectedOffer.vatExclusivePrice.toString(),
currency: selectedOffer.currency,
onSuccess: (response) async {
// Payment result sent successfully, now fetch purchase history
await offersAndDiscountsViewModel.getOffersAndDiscountsHistory(
onSuccess: (historyResponse) {
LoaderBottomSheet.hideLoader();
// Find the purchased offer in the history by matching the offer ID
final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere(
(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),
),
);
},
onError: (error) {
// If history fetch fails, still show success message
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
},
onError: (error) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Payment recorded but confirmation failed. Please contact support."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}
} }

@ -17,6 +17,7 @@ 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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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/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:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -99,7 +100,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context))); return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
} }
print(offer.toJson()); print(vm.offerDetails!.toRawJson());
final status = _getOfferStatus(offer.validTo); final status = _getOfferStatus(offer.validTo);
return SingleChildScrollView( return SingleChildScrollView(
@ -293,7 +294,28 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
text: "Buy Now", text: "Buy Now",
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { 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,
);
},
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page // Set the selected offer and navigate to payment page
viewModel.setSelectedOffer(offer); viewModel.setSelectedOffer(offer);
Navigator.of(context).push( Navigator.of(context).push(

@ -5,10 +5,16 @@ import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_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/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart';
import 'package:hmg_patient_app_new/theme/colors.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/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:provider/provider.dart';
class OffersAndDiscountsHistoryPage extends StatefulWidget { class OffersAndDiscountsHistoryPage extends StatefulWidget {
@ -25,7 +31,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
@override @override
void initState() { void initState() {
scheduleMicrotask(() { scheduleMicrotask(() {
// offersAndDiscountsViewModel.getOffersAndDiscountsHistory(); offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
}); });
super.initState(); super.initState();
} }
@ -36,42 +42,142 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false); offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView( return CollapsingListView(
title: '${LocaleKeys.order.tr(context: context)} ${LocaleKeys.history.tr(context: context)}', title: LocaleKeys.order.tr(context: context),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) { child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
if (offersAndDiscountVM.isHistoryLoading) {
return Container(
padding: EdgeInsets.all(24.h),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
}
final 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 Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Temporarily show a message that history is not available yet SizedBox(height: 24.h),
Container( ListView.separated(
padding: EdgeInsets.all(24.h), shrinkWrap: true,
child: Column( physics: NeverScrollableScrollPhysics(),
children: [ itemCount: historyList.length,
Icon( separatorBuilder: (context, index) => SizedBox(height: 16.h),
Icons.history, itemBuilder: (context, index) {
size: 64.h, final history = historyList[index];
color: AppColors.greyTextColor, return Container(
), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
SizedBox(height: 16.h), child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
'Order history coming soon', children: [
style: TextStyle( AppCustomChipWidget(
fontSize: 18.f, labelText: _getOfferStatus(history),
fontWeight: FontWeight.w700, backgroundColor: _getOfferStatusBgColor(history),
color: AppColors.textColor, textColor: _getOfferStatusTextColor(history),
), labelPadding: EdgeInsetsDirectional.only(start: 12.w, end: 12.w, top: 2.h, bottom: 2.h),
), ),
SizedBox(height: 8.h), SizedBox(width: 8.w),
Text( Row(
'This feature will be available once the history API is ready.', mainAxisAlignment: MainAxisAlignment.spaceBetween,
textAlign: TextAlign.center, children: [
style: TextStyle( Expanded(
fontSize: 14.f, child: (appState.getLanguageID() == 1 ? history.offer.nameAr : history.offer.name).toText16(isBold: true),
color: AppColors.greyTextColor, ),
), ],
), ),
], SizedBox(height: 12.h),
), Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: "Order: ${history.voucherCode}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Purchased: ${_formatDateChipFromDateTime(history.purchaseDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Valid until: ${_formatDateChipFromDateTime(history.validTo)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.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(height: 12.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "View Offer Details",
onPressed: () {},
backgroundColor: AppColors.lightRedButtonColor,
borderColor: AppColors.lightRedButtonColor,
textColor: AppColors.primaryRedColor,
borderWidth: 0.w,
),
),
SizedBox(
width: 9.w,
),
Expanded(
child: CustomButton(
text: "View Order Details",
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsSuccessQrPage(purchaseData: history)));
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
),
)
],
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h);
},
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
], ],
@ -80,4 +186,94 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
), ),
); );
} }
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 _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();
}
}
String _formatDateChipFromDateTime(DateTime date) {
try {
return DateFormat('d MMM yyyy').format(date);
} catch (e) {
return date.toString();
}
}
String _getOfferStatus(dynamic history) {
try {
final now = DateTime.now();
// Check if the offer has expired based on validTo date
if (now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
// Check state for active/available status
if (history.state.toLowerCase() == 'redeemed' || history.state.toLowerCase() == 'used') {
return LocaleKeys.expired.tr(context: context);
}
if (history.state.toLowerCase() == 'active' || history.state.toLowerCase() == 'valid') {
return LocaleKeys.active.tr(context: context);
}
// For pending or other states, show as available
return "Available";
} catch (e) {
return LocaleKeys.active.tr(context: context);
}
}
Color _getOfferStatusBgColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
return AppColors.successColor.withValues(alpha: 0.1);
} else if (status == LocaleKeys.expired.tr(context: context)) {
return AppColors.errorColor.withValues(alpha: 0.1);
} else {
return AppColors.alertColor.withValues(alpha: 0.1);
}
}
Color _getOfferStatusTextColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
return AppColors.successColor;
} else if (status == LocaleKeys.expired.tr(context: context)) {
return AppColors.errorColor;
} else {
return AppColors.alertColor;
}
}
} }

@ -20,6 +20,7 @@ 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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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/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/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -41,10 +42,29 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
@override @override
void initState() { void initState() {
scheduleMicrotask(() {
offersAndDiscountsViewModel.initOffersAndDiscounts();
});
super.initState(); super.initState();
appState = getIt.get<AppState>();
scheduleMicrotask(() async {
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
// Authenticate patient once when visiting the offers page
if (!appState.isPomsAuthTokenValid()) {
await offersAndDiscountsViewModel.authenticatePatient(
onSuccess: (authToken) {
print("POMS Authentication successful: ${authToken.token}");
},
onError: (error) {
print("POMS Authentication failed: $error");
// Authentication failed, but user can still browse offers
},
);
}
// Load offers only if we don't have data yet (prevents double API call)
if (offersAndDiscountsViewModel.offersList.isEmpty) {
offersAndDiscountsViewModel.initOffersAndDiscounts();
}
});
} }
@override @override
@ -294,12 +314,37 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w), labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w),
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
CustomButton( CustomButton(
text: 'Buy Now', text: 'Buy Now',
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () async { onPressed: () async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await offersAndDiscountVM.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
LoaderBottomSheet.hideLoader();
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
LoaderBottomSheet.hideLoader();
return;
}
// Fetch offer details
await offersAndDiscountVM.getOfferDetails( await offersAndDiscountVM.getOfferDetails(
offersAndDiscountVM.filteredOffers[index].id, offersAndDiscountVM.filteredOffers[index].id,
onSuccess: (response) { onSuccess: (response) {

@ -30,10 +30,13 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
void initState() { void initState() {
super.initState(); super.initState();
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
// Initialize the view model and load offers data // Initialize the view model and load offers data only if not already loaded
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false); final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.initOffersAndDiscounts(); // Only initialize if we don't have data yet
if (viewModel.offersList.isEmpty) {
viewModel.initOffersAndDiscounts();
}
}); });
// _startAutoScroll(); // _startAutoScroll();
} }

Loading…
Cancel
Save