Offers And Discounts Apis

offers_discounts
Aamir Muhammad 3 weeks ago
parent 9a93e2ea1a
commit 0b67fb7570

@ -300,6 +300,11 @@ class ApiConsts {
static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations";
// POMS Offers and Discounts API
static final pomsBaseUrl = 'https://poms-uat.odoo.com';
static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
// ************ static values for Api **************** // ************ static values for Api ****************
static final double appVersionID = 21.3; static final double appVersionID = 21.3;
@ -308,6 +313,7 @@ class ApiConsts {
static final String appIpAddress = "10.20.10.20"; static final String appIpAddress = "10.20.10.20";
static final String appGeneralId = "Cs2020@2016\$2958"; static final String appGeneralId = "Cs2020@2016\$2958";
static final String sessionID = 'TMRhVmkGhOsvamErw'; static final String sessionID = 'TMRhVmkGhOsvamErw';
static final pomsApiKey = 'sk-poms-hQPVijTuGbKr1VI8Aa8f85A6AR';
} }
// PACKAGES and OFFERS // PACKAGES and OFFERS
@ -862,8 +868,7 @@ var GET_CUSTOMER_INFO = "VerifyCustomer";
//Pharmacy //Pharmacy
var GET_PHARMACY_CATEGORISE = var GET_PHARMACY_CATEGORISE = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0';
'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0';
var GET_OFFERS_CATEGORISE = 'discountcategories'; var GET_OFFERS_CATEGORISE = 'discountcategories';
var GET_OFFERS_PRODUCTS = 'offerproducts/'; var GET_OFFERS_PRODUCTS = 'offerproducts/';
var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id='; var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=';

@ -25,7 +25,7 @@ class AppState {
_loadProfileImageFromCache(); _loadProfileImageFromCache();
} }
bool isEnabledOffersAndDiscountsCarousel = false; bool isEnabledOffersAndDiscountsCarousel = true;
double userLat = 0.0; double userLat = 0.0;

@ -0,0 +1,265 @@
import 'dart:convert';
class OffersAndDiscountsDetailedResponseModel {
int id;
String ref;
String name;
String nameAr;
Category category;
List<Branch> branches;
double listPrice;
String currency;
String imageUrl;
DateTime validFrom;
DateTime validTo;
String description;
String descriptionAr;
int vatExclusivePrice;
double vatInclusivePrice;
bool requirePatientRegistration;
Images images;
List<OfferLine> offerLines;
OffersAndDiscountsDetailedResponseModel({
required this.id,
required this.ref,
required this.name,
required this.nameAr,
required this.category,
required this.branches,
required this.listPrice,
required this.currency,
required this.imageUrl,
required this.validFrom,
required this.validTo,
required this.description,
required this.descriptionAr,
required this.vatExclusivePrice,
required this.vatInclusivePrice,
required this.requirePatientRegistration,
required this.images,
required this.offerLines,
});
factory OffersAndDiscountsDetailedResponseModel.fromRawJson(String str) => OffersAndDiscountsDetailedResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OffersAndDiscountsDetailedResponseModel.fromJson(Map<String, dynamic> json) => OffersAndDiscountsDetailedResponseModel(
id: json["id"],
ref: json["ref"],
name: json["name"],
nameAr: json["name_ar"],
category: Category.fromJson(json["category"]),
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
listPrice: json["list_price"]?.toDouble(),
currency: json["currency"],
imageUrl: json["image_url"],
validFrom: DateTime.parse(json["valid_from"]),
validTo: DateTime.parse(json["valid_to"]),
description: json["description"],
descriptionAr: json["description_ar"],
vatExclusivePrice: json["vat_exclusive_price"],
vatInclusivePrice: json["vat_inclusive_price"]?.toDouble(),
requirePatientRegistration: json["require_patient_registration"],
images: Images.fromJson(json["images"]),
offerLines: List<OfferLine>.from(json["offer_lines"].map((x) => OfferLine.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"id": id,
"ref": ref,
"name": name,
"name_ar": nameAr,
"category": category.toJson(),
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
"list_price": listPrice,
"currency": currency,
"image_url": imageUrl,
"valid_from": "${validFrom.year.toString().padLeft(4, '0')}-${validFrom.month.toString().padLeft(2, '0')}-${validFrom.day.toString().padLeft(2, '0')}",
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"description": description,
"description_ar": descriptionAr,
"vat_exclusive_price": vatExclusivePrice,
"vat_inclusive_price": vatInclusivePrice,
"require_patient_registration": requirePatientRegistration,
"images": images.toJson(),
"offer_lines": List<dynamic>.from(offerLines.map((x) => x.toJson())),
};
}
class Branch {
int id;
String code;
String name;
String nameAr;
Branch({
required this.id,
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(
id: json["id"],
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"id": id,
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Category {
int id;
bool code;
String name;
String nameAr;
Category({
required this.id,
required this.code,
required this.name,
required this.nameAr,
});
factory Category.fromRawJson(String str) => Category.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Category.fromJson(Map<String, dynamic> json) => Category(
id: json["id"],
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"id": id,
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Images {
List<Banner> main;
List<Banner> banners;
List<Banner> details;
Images({
required this.main,
required this.banners,
required this.details,
});
factory Images.fromRawJson(String str) => Images.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Images.fromJson(Map<String, dynamic> json) => Images(
main: List<Banner>.from(json["main"].map((x) => Banner.fromJson(x))),
banners: List<Banner>.from(json["banners"].map((x) => Banner.fromJson(x))),
details: List<Banner>.from(json["details"].map((x) => Banner.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"main": List<dynamic>.from(main.map((x) => x.toJson())),
"banners": List<dynamic>.from(banners.map((x) => x.toJson())),
"details": List<dynamic>.from(details.map((x) => x.toJson())),
};
}
class Banner {
int id;
String url;
int sequence;
Banner({
required this.id,
required this.url,
required this.sequence,
});
factory Banner.fromRawJson(String str) => Banner.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Banner.fromJson(Map<String, dynamic> json) => Banner(
id: json["id"],
url: json["url"],
sequence: json["sequence"],
);
Map<String, dynamic> toJson() => {
"id": id,
"url": url,
"sequence": sequence,
};
}
class OfferLine {
int sequence;
String lineType;
String procedureCode;
String name;
String nameAr;
int basePrice;
int discountedPrice;
String discountType;
int discountValue;
int quantity;
OfferLine({
required this.sequence,
required this.lineType,
required this.procedureCode,
required this.name,
required this.nameAr,
required this.basePrice,
required this.discountedPrice,
required this.discountType,
required this.discountValue,
required this.quantity,
});
factory OfferLine.fromRawJson(String str) => OfferLine.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferLine.fromJson(Map<String, dynamic> json) => OfferLine(
sequence: json["sequence"],
lineType: json["line_type"],
procedureCode: json["procedure_code"],
name: json["name"],
nameAr: json["name_ar"],
basePrice: json["base_price"],
discountedPrice: json["discounted_price"],
discountType: json["discount_type"],
discountValue: json["discount_value"],
quantity: json["quantity"],
);
Map<String, dynamic> toJson() => {
"sequence": sequence,
"line_type": lineType,
"procedure_code": procedureCode,
"name": name,
"name_ar": nameAr,
"base_price": basePrice,
"discounted_price": discountedPrice,
"discount_type": discountType,
"discount_value": discountValue,
"quantity": quantity,
};
}

@ -1,52 +1,201 @@
import 'dart:convert';
class OffersAndDiscountsResponseModel { class OffersAndDiscountsResponseModel {
int? id; List<OfferData> data;
String? title; Meta meta;
String? description;
String? imageUrl;
String? facilityType; // HMC, HMG, or Both
String? startDate;
String? endDate;
String? discount;
bool? isActive;
OffersAndDiscountsResponseModel({ OffersAndDiscountsResponseModel({
this.id, required this.data,
this.title, required this.meta,
this.description,
this.imageUrl,
this.facilityType,
this.startDate,
this.endDate,
this.discount,
this.isActive,
}); });
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) { factory OffersAndDiscountsResponseModel.fromRawJson(String str) => OffersAndDiscountsResponseModel.fromJson(json.decode(str));
return OffersAndDiscountsResponseModel(
id: json['ID'] as int?,
title: json['Title'] as String?,
description: json['Description'] as String?,
imageUrl: json['ImageURL'] as String?,
facilityType: json['FacilityType'] as String?,
startDate: json['StartDate'] as String?,
endDate: json['EndDate'] as String?,
discount: json['Discount'] as String?,
isActive: json['IsActive'] as bool?,
);
}
Map<String, dynamic> toJson() { String toRawJson() => json.encode(toJson());
return {
'ID': id, factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) => OffersAndDiscountsResponseModel(
'Title': title, data: List<OfferData>.from(json["data"].map((x) => OfferData.fromJson(x))),
'Description': description, meta: Meta.fromJson(json["meta"]),
'ImageURL': imageUrl, );
'FacilityType': facilityType,
'StartDate': startDate, Map<String, dynamic> toJson() => {
'EndDate': endDate, "data": List<dynamic>.from(data.map((x) => x.toJson())),
'Discount': discount, "meta": meta.toJson(),
'IsActive': isActive, };
}; }
}
class OfferData {
int id;
String ref;
String name;
String nameAr;
Category category;
List<Branch> branches;
double listPrice;
Currency currency;
String imageUrl;
DateTime validFrom;
DateTime validTo;
OfferData({
required this.id,
required this.ref,
required this.name,
required this.nameAr,
required this.category,
required this.branches,
required this.listPrice,
required this.currency,
required this.imageUrl,
required this.validFrom,
required this.validTo,
});
factory OfferData.fromRawJson(String str) => OfferData.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferData.fromJson(Map<String, dynamic> json) => OfferData(
id: json["id"],
ref: json["ref"],
name: json["name"],
nameAr: json["name_ar"],
category: Category.fromJson(json["category"]),
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
listPrice: json["list_price"]?.toDouble(),
currency: currencyValues.map[json["currency"]]!,
imageUrl: json["image_url"],
validFrom: DateTime.parse(json["valid_from"]),
validTo: DateTime.parse(json["valid_to"]),
);
Map<String, dynamic> toJson() => {
"id": id,
"ref": ref,
"name": name,
"name_ar": nameAr,
"category": category.toJson(),
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
"list_price": listPrice,
"currency": currencyValues.reverse[currency],
"image_url": imageUrl,
"valid_from": "${validFrom.year.toString().padLeft(4, '0')}-${validFrom.month.toString().padLeft(2, '0')}-${validFrom.day.toString().padLeft(2, '0')}",
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
};
} }
class Branch {
int id;
String code;
String name;
String nameAr;
Branch({
required this.id,
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(
id: json["id"],
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"id": id,
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Category {
int id;
bool code;
String name;
String nameAr;
Category({
required this.id,
required this.code,
required this.name,
required this.nameAr,
});
factory Category.fromRawJson(String str) => Category.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Category.fromJson(Map<String, dynamic> json) => Category(
id: json["id"],
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"id": id,
"code": code,
"name": name,
"name_ar": nameAr,
};
}
enum Currency {
SAR
}
final currencyValues = EnumValues({
"SAR": Currency.SAR
});
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,
};
}
class EnumValues<T> {
Map<String, T> map;
late Map<T, String> reverseMap;
EnumValues(this.map);
Map<T, String> get reverse {
reverseMap = map.map((k, v) => MapEntry(v, k));
return reverseMap;
}
}

@ -4,10 +4,14 @@ import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; 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/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class OffersAndDiscountsRepo { abstract class OffersAndDiscountsRepo {
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts(); Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts();
Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId);
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory(); Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory();
} }
@ -18,42 +22,91 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient}); OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override @override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts() async { Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts() async {
Map<String, dynamic> mapDevice = {}; Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
try { try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse; GenericApiModel<OffersAndDiscountsResponseModel>? apiResponse;
Failure? failure; Failure? failure;
await apiClient.post(
GET_OFFERS_AND_DISCOUNTS, // TODO: Replace with actual API endpoint await apiClient.get(
body: mapDevice, ApiConsts.pomsGetHospitalOffers,
onFailure: (error, statusCode, {messageStatus, failureType}) { apiHeaders: headers,
failure = failureType; isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode) {
failure = ServerFailure(error);
}, },
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode) {
try { try {
final list = response['List_OffersAndDiscounts']; final offersResponse = OffersAndDiscountsResponseModel.fromJson(response);
if (list == null || list.isEmpty) {
throw Exception("offers and discounts list is empty"); apiResponse = GenericApiModel<OffersAndDiscountsResponseModel>(
} messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: offersResponse,
);
} catch (e) {
loggerService.logError("Error parsing offers response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
final offers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>(); if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in getOffersAndDiscounts: $e");
return Left(UnknownFailure(e.toString()));
}
}
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>( @override
messageStatus: messageStatus, Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
try {
GenericApiModel<OffersAndDiscountsDetailedResponseModel>? apiResponse;
Failure? failure;
await apiClient.get(
'${ApiConsts.pomsGetHospitalOffersDetails}/$offerId',
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode) {
try {
final offerDetails = OffersAndDiscountsDetailedResponseModel.fromJson(response);
apiResponse = GenericApiModel<OffersAndDiscountsDetailedResponseModel>(
messageStatus: 1,
statusCode: statusCode, statusCode: statusCode,
errorMessage: null, errorMessage: null,
data: offers, data: offerDetails,
); );
} catch (e) { } catch (e) {
loggerService.logError("Error parsing offer details 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 getOfferDetails: $e");
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }
@ -99,4 +152,3 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
} }
} }
} }

@ -1,320 +1,37 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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/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';
class OffersAndDiscountsViewModel extends ChangeNotifier { class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false; bool isOffersLoading = false;
bool isHistoryLoading = false; bool isHistoryLoading = false;
bool isOfferDetailsLoading = false;
List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers' List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers'
String searchQuery = ''; String searchQuery = '';
OffersAndDiscountsRepo offersAndDiscountsRepo; OffersAndDiscountsRepo offersAndDiscountsRepo;
ErrorHandlerService errorHandlerService; ErrorHandlerService errorHandlerService;
List<OffersAndDiscountsResponseModel> offersList = []; List<OfferData> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = []; List<OffersAndDiscountsResponseModel> historyList = [];
OffersAndDiscountsDetailedResponseModel? offerDetails;
Meta? meta;
OffersAndDiscountsViewModel({ OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo, required this.offersAndDiscountsRepo,
required this.errorHandlerService, required this.errorHandlerService,
}) { }) {
_initializeDummyData();
}
// Initialize with dummy data for testing
void _initializeDummyData() {
offersList = [
OffersAndDiscountsResponseModel(
id: 1,
title: 'Summer Health Checkup Package',
description: 'Complete health screening with blood tests, X-ray, and consultation. Get 30% off on all diagnostic services.',
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
facilityType: 'Female',
startDate: '2026-05-01',
endDate: '2026-08-31',
discount: '30% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 2,
title: 'Maternity Care Special',
description: 'Comprehensive prenatal care package including ultrasound, consultations, and postnatal support.',
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-05-01',
endDate: '2026-12-31',
discount: '25% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 3,
title: 'Skin Treatment Package',
description: 'Advanced dermatology services including acne treatment, anti-aging procedures, and skin rejuvenation.',
imageUrl: 'https://images.unsplash.com/photo-1570172619644-dfd03ed5d881?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-15',
endDate: '2026-07-15',
discount: '40% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 4,
title: 'CT & MRI Scanning Discount',
description: 'State-of-the-art imaging services with latest technology. Book your scan today!',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-06-30',
discount: '20% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 5,
title: 'Women\'s Health Screening',
description: 'Comprehensive health package designed specifically for women including mammography, bone density scan, and hormone tests.',
imageUrl: 'https://images.unsplash.com/photo-1559757175-5700dde675bc?w=800',
facilityType: 'Female',
startDate: '2026-05-10',
endDate: '2026-09-30',
discount: '35% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 6,
title: 'Gynecology Consultation Special',
description: 'Free follow-up consultation with every initial gynecology visit. Expert care for all women\'s health needs.',
imageUrl: 'https://images.unsplash.com/photo-1631217868264-e5b90bb7e133?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-05-01',
endDate: '2026-07-31',
discount: 'FREE Follow-up',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 7,
title: 'Laser Hair Removal Package',
description: 'Professional laser hair removal treatment with latest technology. Multiple sessions available.',
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-20',
endDate: '2026-10-20',
discount: '50% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 8,
title: 'X-Ray & Ultrasound Combo',
description: 'Get both X-ray and ultrasound services at a discounted price. Quick results guaranteed.',
imageUrl: 'https://images.unsplash.com/photo-1530497610245-94d3c16cda28?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-08-15',
discount: '15% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 9,
title: 'Complete Wellness Package',
description: 'Full body checkup including all major tests, consultations, and health assessment report.',
imageUrl: 'https://images.unsplash.com/photo-1505751172876-fa1923c5c528?w=800',
facilityType: 'Both',
startDate: '2026-05-01',
endDate: '2026-12-31',
discount: '45% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 10,
title: 'Botox & Filler Treatment',
description: 'Anti-aging treatments with certified dermatologists. Natural-looking results guaranteed.',
imageUrl: 'https://images.unsplash.com/photo-1515377905703-c4788e51af15?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-15',
endDate: '2026-06-15',
discount: '30% OFF',
isActive: true,
),
];
// Past offers for history
historyList = [
OffersAndDiscountsResponseModel(
id: 101,
title: 'Winter Health Campaign',
description: 'Flu shots and winter wellness packages that were offered during the winter season.',
imageUrl: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=800',
facilityType: 'Both',
startDate: '2025-12-01',
endDate: '2026-02-28',
discount: '25% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 102,
title: 'Valentine\'s Day Couple Checkup',
description: 'Special couple health screening packages offered for Valentine\'s Day.',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Both',
startDate: '2026-02-01',
endDate: '2026-02-14',
discount: '40% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 103,
title: 'Spring Skin Renewal',
description: 'Spring special dermatology treatments for skin rejuvenation.',
imageUrl: 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800',
facilityType: 'Dermatology',
startDate: '2026-03-01',
endDate: '2026-04-30',
discount: '30% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 104,
title: 'Summer Wellness Package - Active',
description: 'Active offer for comprehensive summer health checkup with multiple tests.',
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
facilityType: 'Female',
startDate: '2026-05-01',
endDate: '2026-08-31',
discount: '35% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 105,
title: 'Kids Health Screening - Active',
description: 'Currently active pediatric health screening package for children.',
imageUrl: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=800',
facilityType: 'Both',
startDate: '2026-05-05',
endDate: '2026-09-30',
discount: '20% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 106,
title: 'Laser Treatment Offer - Expired',
description: 'Laser hair removal package that has expired.',
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
facilityType: 'Dermatology',
startDate: '2026-01-01',
endDate: '2026-03-31',
discount: '50% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 107,
title: 'Cardiology Consultation Package',
description: 'Heart health checkup with ECG and consultation.',
imageUrl: 'https://images.unsplash.com/photo-1628348068343-c6a848d2b6dd?w=800',
facilityType: 'Both',
startDate: '2025-11-01',
endDate: '2026-01-31',
discount: '25% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 108,
title: 'Dental Care Special - Active',
description: 'Comprehensive dental checkup and cleaning package.',
imageUrl: 'https://images.unsplash.com/photo-1606811971618-4486d14f3f99?w=800',
facilityType: 'Both',
startDate: '2026-04-01',
endDate: '2026-12-31',
discount: '30% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 109,
title: 'Eye Care Package - Expired',
description: 'Complete eye examination with vision testing.',
imageUrl: 'https://images.unsplash.com/photo-1516534775068-ba3e7458af70?w=800',
facilityType: 'Both',
startDate: '2025-10-01',
endDate: '2025-12-31',
discount: '20% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 110,
title: 'Maternity Care Premium',
description: 'Premium prenatal and postnatal care package.',
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-01-15',
endDate: '2026-06-30',
discount: '40% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 111,
title: 'Blood Test Special - Expired',
description: 'Comprehensive blood work panel at discounted rates.',
imageUrl: 'https://images.unsplash.com/photo-1579154204601-01588f351e67?w=800',
facilityType: 'Both',
startDate: '2025-09-01',
endDate: '2025-11-30',
discount: '15% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 112,
title: 'Vaccination Drive',
description: 'Special vaccination package for adults and children.',
imageUrl: 'https://images.unsplash.com/photo-1587854692152-cbe660dbde88?w=800',
facilityType: 'Both',
startDate: '2026-03-01',
endDate: '2026-05-05',
discount: '10% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 113,
title: 'Radiology Imaging Package - Active',
description: 'CT, MRI, and X-ray services at reduced prices.',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-10-31',
discount: '25% OFF',
isActive: false, // Changed to false - will show as "Availed"
),
OffersAndDiscountsResponseModel(
id: 114,
title: 'Physiotherapy Sessions',
description: 'Package of 10 physiotherapy sessions.',
imageUrl: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=800',
facilityType: 'Both',
startDate: '2025-12-01',
endDate: '2026-04-01',
discount: '35% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 115,
title: 'Nutrition Consultation Bundle - Active',
description: 'Series of nutrition and diet consultation sessions.',
imageUrl: 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=800',
facilityType: 'Both',
startDate: '2026-05-01',
endDate: '2026-11-30',
discount: '20% OFF',
isActive: true,
),
];
} }
// For loading state compatibility with existing code // For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading; bool get isRegionListLoading => isOffersLoading;
initOffersAndDiscounts() { initOffersAndDiscounts() {
// Don't clear the list - keep dummy data until API returns offersList.clear();
// Don't show loading state since we have dummy data isOffersLoading = true;
// offersList.clear();
// isOffersLoading = true;
notifyListeners(); notifyListeners();
getOffersAndDiscounts(); getOffersAndDiscounts();
} }
@ -340,6 +57,9 @@ 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;
notifyListeners();
final result = await offersAndDiscountsRepo.getOffersAndDiscounts(); final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
result.fold( result.fold(
@ -347,15 +67,16 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
isOffersLoading = false; isOffersLoading = false;
notifyListeners(); notifyListeners();
// Keep dummy data if API fails - don't clear the list // 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) {
isOffersLoading = false; isOffersLoading = false;
if (response.data != null && response.data!.isNotEmpty) { if (response.data != null) {
offersList = response.data!; offersList = response.data!.data;
meta = response.data!.meta;
} }
// If API returns empty or null, keep the dummy data // If API returns empty or null, keep the dummy data
notifyListeners(); notifyListeners();
@ -366,6 +87,34 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
); );
} }
Future<void> getOfferDetails(int offerId, {Function(dynamic)? onSuccess, Function(String)? onError}) async {
isOfferDetailsLoading = true;
notifyListeners();
final result = await offersAndDiscountsRepo.getOfferDetails(offerId);
result.fold(
(failure) async {
isOfferDetailsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isOfferDetailsLoading = false;
if (response.data != null) {
offerDetails = response.data;
}
notifyListeners();
if (onSuccess != null) {
onSuccess(response);
}
},
);
}
Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isHistoryLoading = true; isHistoryLoading = true;
notifyListeners(); notifyListeners();
@ -396,25 +145,24 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
); );
} }
List<OffersAndDiscountsResponseModel> get filteredOffers { List<OfferData> get filteredOffers {
List<OffersAndDiscountsResponseModel> filtered = offersList; List<OfferData> filtered = offersList;
// Filter by facility types (multi-select support) // Filter by facility types (multi-select support)
if (!selectedFacilities.contains('All Offers')) { if (!selectedFacilities.contains('All Offers')) {
filtered = filtered.where((offer) { filtered = filtered.where((offer) {
// Check if offer's facility type matches any of the selected types // Check if offer's branches match any of the selected facility types
return selectedFacilities.any((selectedType) => // You can customize this logic based on how you want to filter by facility
offer.facilityType == selectedType || offer.facilityType == 'Both' return true; // For now, show all when filtering is needed
);
}).toList(); }).toList();
} }
// Filter by search query // Filter by search query
if (searchQuery.isNotEmpty) { if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) { filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false; final titleMatch = offer.name.toLowerCase().contains(searchQuery.toLowerCase());
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false; final titleArMatch = offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
return titleMatch || descriptionMatch; return titleMatch || titleArMatch;
}).toList(); }).toList();
} }
@ -424,26 +172,14 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
List<OffersAndDiscountsResponseModel> get filteredHistory { List<OffersAndDiscountsResponseModel> get filteredHistory {
List<OffersAndDiscountsResponseModel> filtered = historyList; List<OffersAndDiscountsResponseModel> filtered = historyList;
// Filter by facility types (multi-select support)
if (!selectedFacilities.contains('All Offers')) {
filtered = filtered.where((offer) {
// Check if offer's facility type matches any of the selected types
return selectedFacilities.any((selectedType) =>
offer.facilityType == selectedType || offer.facilityType == 'Both'
);
}).toList();
}
// Filter by search query // Filter by search query
if (searchQuery.isNotEmpty) { if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) { filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false; // Assuming history list uses the old structure - update when API is available
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false; return true; // Placeholder for now
return titleMatch || descriptionMatch;
}).toList(); }).toList();
} }
return filtered; return filtered;
} }
} }

@ -1,53 +1,61 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/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/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.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/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:provider/provider.dart';
class OffersAndDiscountsDetailedPage extends StatelessWidget { class OffersAndDiscountsDetailedPage extends StatefulWidget {
final OffersAndDiscountsResponseModel offer; final int offerId;
const OffersAndDiscountsDetailedPage({ const OffersAndDiscountsDetailedPage({
super.key, super.key,
required this.offer, required this.offerId,
}); });
String _formatDateString(String? dateString) { @override
if (dateString == null || dateString.isEmpty) return ''; State<OffersAndDiscountsDetailedPage> createState() => _OffersAndDiscountsDetailedPageState();
}
class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetailedPage> {
late OffersAndDiscountsViewModel viewModel;
late AppState appState;
@override
void initState() {
super.initState();
scheduleMicrotask(() {
viewModel.getOfferDetails(widget.offerId);
});
}
String _formatDateString(DateTime date) {
try { try {
DateTime date = DateTime.parse(dateString);
return DateFormat('d MMM, yyyy').format(date); return DateFormat('d MMM, yyyy').format(date);
} catch (e) { } catch (e) {
return dateString; return '';
} }
} }
// Determine the status of the offer based on dates and isActive flag // Determine the status of the offer based on dates
String _getOfferStatus(String? endDate, bool? isActive) { String _getOfferStatus(DateTime validTo) {
if (endDate == null || endDate.isEmpty) return 'Expired'; DateTime now = DateTime.now();
try { if (now.isAfter(validTo)) {
DateTime end = DateTime.parse(endDate);
DateTime now = DateTime.now();
if (now.isAfter(end)) {
return 'Expired';
} else {
if (isActive == true) {
return 'Active';
} else {
return 'Availed';
}
}
} catch (e) {
return 'Expired'; return 'Expired';
} else {
return 'Active';
} }
} }
@ -55,8 +63,6 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
switch (status) { switch (status) {
case 'Active': case 'Active':
return AppColors.successColor.withValues(alpha: 0.1); return AppColors.successColor.withValues(alpha: 0.1);
case 'Availed':
return AppColors.infoColor.withValues(alpha: 0.1);
case 'Expired': case 'Expired':
return AppColors.errorColor.withValues(alpha: 0.1); return AppColors.errorColor.withValues(alpha: 0.1);
default: default:
@ -68,8 +74,6 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
switch (status) { switch (status) {
case 'Active': case 'Active':
return AppColors.successColor; return AppColors.successColor;
case 'Availed':
return AppColors.infoColor;
case 'Expired': case 'Expired':
return AppColors.errorColor; return AppColors.errorColor;
default: default:
@ -79,22 +83,57 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final status = _getOfferStatus(offer.endDate, offer.isActive); appState = getIt.get<AppState>();
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView( return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}", title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}",
child: SingleChildScrollView( child: Consumer<OffersAndDiscountsViewModel>(
child: Column( builder: (context, vm, child) {
crossAxisAlignment: CrossAxisAlignment.start, if (vm.isOfferDetailsLoading) {
children: [ return Center(child: Utils.getLoadingWidget());
// Offer Image }
if (offer.imageUrl != null)
Stack( final offer = vm.offerDetails;
children: [ if (offer == null) {
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
}
final status = _getOfferStatus(offer.validTo);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Offer Image Carousel or Single Image
if (offer.images.banners.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork(
url: offer.images.banners.first.url,
iconColor: AppColors.blackBgColor,
width: double.infinity,
height: 250.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 250.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.greyColor,
borderRadius: 24.h,
),
child: Center(
child: Icon(Icons.image_not_supported, size: 64.h),
),
);
},
),
)
else if (offer.imageUrl.isNotEmpty)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(24.h), borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork( child: Utils.buildImgWithNetwork(
url: offer.imageUrl!, url: offer.imageUrl,
iconColor: AppColors.blackBgColor, iconColor: AppColors.blackBgColor,
width: double.infinity, width: double.infinity,
height: 250.h, height: 250.h,
@ -113,112 +152,169 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
}, },
), ),
), ),
// Discount badge at top right on image
// if (offer.discount != null)
// Positioned(
// top: 16.h,
// right: 16.w,
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
// decoration: BoxDecoration(
// color: AppColors.primaryRedColor,
// borderRadius: BorderRadius.circular(8.r),
// ),
// child: Text(
// offer.discount!,
// style: TextStyle(
// fontSize: 14.f,
// fontWeight: FontWeight.w700,
// color: AppColors.whiteColor,
// ),
// ),
// ),
// ),
],
),
SizedBox(height: 24.h), SizedBox(height: 24.h),
// Title // Title
if (offer.title != null) (appState.getLanguageID() == 1 ? offer.nameAr : offer.name).toText24(
offer.title!.toText24( isBold: true,
isBold: true, fontWeight: FontWeight.w700,
fontWeight: FontWeight.w700, ),
),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Chips Row (Valid Till and Status) // Price Information
Wrap( Row(
spacing: 12.w, children: [
runSpacing: 12.h, "Price: ".toText18(
children: [ isBold: true,
// Valid till chip weight: FontWeight.w700,
if (offer.endDate != null) ),
AppCustomChipWidget( "${offer.listPrice} ${offer.currency}".toText18(
labelText: "Valid till ${_formatDateString(offer.endDate)}", color: AppColors.primaryRedColor,
backgroundColor: AppColors.chipBgColor, isBold: true,
textColor: AppColors.textColor, weight: FontWeight.w700,
icon: AppAssets.calendar, ),
iconSize: 14.h, ],
iconColor: AppColors.textColor, ),
isEnglishOnly: true,
), SizedBox(height: 16.h),
// Status chip
AppCustomChipWidget( // Chips Row (Valid Till and Status)
labelText: status, Wrap(
backgroundColor: _getStatusBgColor(status), spacing: 12.w,
textColor: _getStatusTextColor(status), runSpacing: 12.h,
children: [
// Valid till chip
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.validTo)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 14.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Status chip
AppCustomChipWidget(
labelText: status,
backgroundColor: _getStatusBgColor(status),
textColor: _getStatusTextColor(status),
),
// Category chip
AppCustomChipWidget(
labelText: appState.getLanguageID() == 1 ? offer.category.nameAr : offer.category.name,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
),
],
), ),
// Facility type chip
if (offer.facilityType != null)
AppCustomChipWidget(
labelText: offer.facilityType!,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
),
],
),
SizedBox(height: 32.h), SizedBox(height: 24.h),
// Description Heading // Branches Section
LocaleKeys.description.tr(context: context).toText18( if (offer.branches.isNotEmpty) ...[
"Available at".toText18(
isBold: true,
weight: FontWeight.w700,
),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: offer.branches.map((branch) {
return AppCustomChipWidget(
labelText: appState.getLanguageID() == 1 ? branch.nameAr : branch.name,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
);
}).toList(),
),
SizedBox(height: 24.h),
],
// Description Heading
LocaleKeys.description.tr(context: context).toText18(
isBold: true, isBold: true,
weight: FontWeight.w700, weight: FontWeight.w700,
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
// Description Content // Description Content
if (offer.description != null) (appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description).toText16(
offer.description!.toText16( color: AppColors.greyTextColor,
color: AppColors.greyTextColor, height: 1.5,
height: 1.5, ),
),
SizedBox(height: 24.h),
SizedBox(height: 32.h),
// Offer Lines Section
// Buy Now Button if (offer.offerLines.isNotEmpty) ...[
CustomButton( "Included Services".toText18(
text: "Buy Now", isBold: true,
icon: AppAssets.shoppingCart, weight: FontWeight.w700,
iconColor: AppColors.whiteColor, ),
onPressed: () { SizedBox(height: 12.h),
// Handle buy now action ...offer.offerLines.map((line) {
}, return Container(
backgroundColor: AppColors.primaryRedColor, margin: EdgeInsets.only(bottom: 12.h),
borderColor: AppColors.primaryRedColor, padding: EdgeInsets.all(12.h),
textColor: AppColors.whiteColor, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
fontSize: 16.f, color: AppColors.whiteColor,
isBold: true, borderRadius: 12.h,
borderRadius: 12.r, hasShadow: true,
height: 56.h, ),
), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 24.h), children: [
], (appState.getLanguageID() == 1 ? line.nameAr : line.name).toText16(
).paddingSymmetrical(24.h, 0.h), isBold: true,
weight: FontWeight.w600,
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Quantity: ${line.quantity}".toText14(
color: AppColors.greyTextColor,
),
if (line.discountValue > 0)
"${line.discountValue}% OFF".toText14(
color: AppColors.primaryRedColor,
isBold: true,
),
],
),
],
),
);
}),
SizedBox(height: 24.h),
],
// Buy Now Button
CustomButton(
text: "Buy Now",
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
// Handle buy now action
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.h, 0.h),
);
},
), ),
); );
} }

@ -2,22 +2,13 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/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/offers_and_discounts_detailed_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 {
@ -34,69 +25,11 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
@override @override
void initState() { void initState() {
scheduleMicrotask(() { scheduleMicrotask(() {
offersAndDiscountsViewModel.getOffersAndDiscountsHistory(); // offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
}); });
super.initState(); super.initState();
} }
String _formatDateString(String? dateString) {
if (dateString == null || dateString.isEmpty) return '';
try {
DateTime date = DateTime.parse(dateString);
return DateFormat('d MMM, yyyy').format(date);
} catch (e) {
return dateString;
}
}
// Determine the status of the offer based on dates and isActive flag
String _getOfferStatus(String? endDate, bool? isActive) {
if (endDate == null || endDate.isEmpty) return 'Expired';
try {
DateTime end = DateTime.parse(endDate);
DateTime now = DateTime.now();
if (now.isAfter(end)) {
return 'Expired';
} else {
// If end date is in future, check isActive flag
if (isActive == true) {
return 'Active';
} else {
return 'Availed';
}
}
} catch (e) {
return 'Expired';
}
}
Color _getStatusBgColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor.withValues(alpha: 0.1);
case 'Availed':
return AppColors.infoColor.withValues(alpha: 0.1);
case 'Expired':
return AppColors.errorColor.withValues(alpha: 0.1);
default:
return AppColors.greyColor;
}
}
Color _getStatusTextColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor;
case 'Availed':
return AppColors.infoColor;
case 'Expired':
return AppColors.errorColor;
default:
return AppColors.textColor;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
@ -109,179 +42,37 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
offersAndDiscountVM.isHistoryLoading // Temporarily show a message that history is not available yet
? Container( Container(
height: 200.h, padding: EdgeInsets.all(24.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( child: Column(
color: AppColors.whiteColor, children: [
borderRadius: 24.h, Icon(
hasShadow: true, Icons.history,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
Text(
'Order history coming soon',
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w700,
color: AppColors.textColor,
),
),
SizedBox(height: 8.h),
Text(
'This feature will be available once the history API is ready.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
), ),
child: Center(child: Utils.getLoadingWidget()),
).paddingSymmetrical(24.h, 0.h)
: ListView.separated(
padding: EdgeInsets.only(top: 12.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: offersAndDiscountVM.filteredHistory.isNotEmpty ? offersAndDiscountVM.filteredHistory.length : 1,
itemBuilder: (context, index) {
if (offersAndDiscountVM.filteredHistory.isEmpty) {
return Utils.getNoDataWidget(context, noDataText: LocaleKeys.noDataAvailable.tr(context: context));
}
final offer = offersAndDiscountVM.filteredHistory[index];
final status = _getOfferStatus(offer.endDate, offer.isActive);
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Stack(
children: [
Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title with space for status chip
Padding(
padding: EdgeInsets.only(right: 80.w),
child: offer.title?.toText18(
isBold: true,
weight: FontWeight.w700,
) ??
SizedBox(),
),
SizedBox(height: 12.h),
// Info chips
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
// Start Date chip
if (offer.startDate != null)
AppCustomChipWidget(
labelText: _formatDateString(offer.startDate),
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 12.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Hospital/Facility chip
if (offer.facilityType != null)
AppCustomChipWidget(
labelText: offer.facilityType!,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
),
// Purchase date chip (using startDate as purchased date)
if (offer.startDate != null)
AppCustomChipWidget(
labelText: "Purchased ${_formatDateString(offer.startDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
isEnglishOnly: true,
),
// Valid until chip
if (offer.endDate != null)
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.endDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 12.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Order ID chip (using offer.id)
if (offer.id != null)
AppCustomChipWidget(
labelText: "ID: #${offer.id}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
isEnglishOnly: true,
),
],
),
SizedBox(height: 16.h),
// Action buttons
Row(
children: [
Expanded(
child: CustomButton(
text: 'View Offer Details',
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(
offer: offersAndDiscountVM.filteredOffers[index],
),
),
);
},
backgroundColor: AppColors.errorColor.withValues(alpha: 0.1),
borderColor: Colors.transparent,
textColor: AppColors.errorColor,
fontSize: 12.f,
isBold: true,
borderRadius: 12.r,
height: 40.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: 'View Order Details',
onPressed: () {
// Handle view order details
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
fontSize: 12.f,
isBold: true,
borderRadius: 12.r,
height: 40.h,
),
),
],
),
],
),
),
// Status chip at top right
Positioned(
top: 16.h,
right: 16.w,
child: AppCustomChipWidget(
labelText: status,
backgroundColor: _getStatusBgColor(status),
textColor: _getStatusTextColor(status),
),
),
],
),
).paddingSymmetrical(24.h, 0.h),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
), ),
],
),
),
SizedBox(height: 24.h), SizedBox(height: 24.h),
], ],
); );

@ -150,7 +150,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (offersAndDiscountVM.filteredOffers[index].imageUrl != null) if (offersAndDiscountVM.filteredOffers[index].imageUrl.isNotEmpty)
Expanded( Expanded(
flex: 3, flex: 3,
child: Stack( child: Stack(
@ -159,7 +159,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)), borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
child: SizedBox.expand( child: SizedBox.expand(
child: Utils.buildImgWithNetwork( child: Utils.buildImgWithNetwork(
url: offersAndDiscountVM.filteredOffers[index].imageUrl!, url: offersAndDiscountVM.filteredOffers[index].imageUrl,
iconColor: AppColors.blackBgColor, iconColor: AppColors.blackBgColor,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
@ -176,32 +176,11 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: OffersAndDiscountsDetailedPage( page: OffersAndDiscountsDetailedPage(
offer: offersAndDiscountVM.filteredOffers[index], offerId: offersAndDiscountVM.filteredOffers[index].id,
), ),
), ),
); );
}), }),
// Discount badge at top right
// if (offersAndDiscountVM.filteredOffers[index].discount != null)
// Positioned(
// top: 8.h,
// right: 8.w,
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: AppColors.primaryRedColor,
// borderRadius: BorderRadius.circular(6.r),
// ),
// child: Text(
// offersAndDiscountVM.filteredOffers[index].discount!,
// style: TextStyle(
// fontSize: 10.f,
// fontWeight: FontWeight.w700,
// color: AppColors.whiteColor,
// ),
// ),
// ),
// ),
], ],
), ),
), ),
@ -213,38 +192,43 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (offersAndDiscountVM.filteredOffers[index].title != null) Text(
Text( appState.getLanguageID() == 1
offersAndDiscountVM.filteredOffers[index].title!, ? offersAndDiscountVM.filteredOffers[index].nameAr
style: TextStyle( : offersAndDiscountVM.filteredOffers[index].name,
fontSize: 14.f, style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14.f,
color: AppColors.textColor, fontWeight: FontWeight.w700,
), color: AppColors.textColor,
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h), SizedBox(height: 8.h),
// Valid till chip // Valid till chip
if (offersAndDiscountVM.filteredOffers[index].endDate != null) AppCustomChipWidget(
AppCustomChipWidget( labelText: "Valid till ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].validTo.toString())}",
labelText: "Valid till ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].endDate ?? "")}", backgroundColor: AppColors.chipBgColor,
// labelText: "Valid till ${_formatDateString(offersAndDiscountVM.filteredOffers[index].endDate)}", textColor: AppColors.textColor,
backgroundColor: AppColors.chipBgColor, icon: AppAssets.calendar,
textColor: AppColors.textColor, iconSize: 12.h,
icon: AppAssets.calendar, iconColor: AppColors.textColor,
iconSize: 12.h, isEnglishOnly: true,
iconColor: AppColors.textColor, labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w),
isEnglishOnly: true, ),
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: () { onPressed: () {
// Handle buy now action Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(
offerId: offersAndDiscountVM.filteredOffers[index].id,
),
),
);
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor,

Loading…
Cancel
Save