Offers And Discounts Apis

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

@ -300,6 +300,11 @@ class ApiConsts {
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 final double appVersionID = 21.3;
@ -308,6 +313,7 @@ class ApiConsts {
static final String appIpAddress = "10.20.10.20";
static final String appGeneralId = "Cs2020@2016\$2958";
static final String sessionID = 'TMRhVmkGhOsvamErw';
static final pomsApiKey = 'sk-poms-hQPVijTuGbKr1VI8Aa8f85A6AR';
}
// PACKAGES and OFFERS
@ -862,8 +868,7 @@ var GET_CUSTOMER_INFO = "VerifyCustomer";
//Pharmacy
var GET_PHARMACY_CATEGORISE =
'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0';
var GET_PHARMACY_CATEGORISE = '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_PRODUCTS = 'offerproducts/';
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();
}
bool isEnabledOffersAndDiscountsCarousel = false;
bool isEnabledOffersAndDiscountsCarousel = true;
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 {
int? id;
String? title;
String? description;
String? imageUrl;
String? facilityType; // HMC, HMG, or Both
String? startDate;
String? endDate;
String? discount;
bool? isActive;
List<OfferData> data;
Meta meta;
OffersAndDiscountsResponseModel({
this.id,
this.title,
this.description,
this.imageUrl,
this.facilityType,
this.startDate,
this.endDate,
this.discount,
this.isActive,
required this.data,
required this.meta,
});
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) {
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?,
);
}
factory OffersAndDiscountsResponseModel.fromRawJson(String str) => OffersAndDiscountsResponseModel.fromJson(json.decode(str));
Map<String, dynamic> toJson() {
return {
'ID': id,
'Title': title,
'Description': description,
'ImageURL': imageUrl,
'FacilityType': facilityType,
'StartDate': startDate,
'EndDate': endDate,
'Discount': discount,
'IsActive': isActive,
};
}
String toRawJson() => json.encode(toJson());
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) => OffersAndDiscountsResponseModel(
data: List<OfferData>.from(json["data"].map((x) => OfferData.fromJson(x))),
meta: Meta.fromJson(json["meta"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
};
}
class OfferData {
int id;
String ref;
String name;
String nameAr;
Category category;
List<Branch> branches;
double listPrice;
Currency currency;
String imageUrl;
DateTime validFrom;
DateTime validTo;
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/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_detailed_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
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();
}
@ -18,42 +22,91 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts() async {
Map<String, dynamic> mapDevice = {};
Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts() async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
GenericApiModel<OffersAndDiscountsResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
GET_OFFERS_AND_DISCOUNTS, // TODO: Replace with actual API endpoint
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
await apiClient.get(
ApiConsts.pomsGetHospitalOffers,
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
onSuccess: (response, statusCode) {
try {
final list = response['List_OffersAndDiscounts'];
if (list == null || list.isEmpty) {
throw Exception("offers and discounts list is empty");
}
final offersResponse = OffersAndDiscountsResponseModel.fromJson(response);
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>>(
messageStatus: messageStatus,
@override
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,
errorMessage: null,
data: offers,
data: offerDetails,
);
} catch (e) {
loggerService.logError("Error parsing offer details 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 getOfferDetails: $e");
return Left(UnknownFailure(e.toString()));
}
}
@ -99,4 +152,3 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
}
}
}

@ -1,320 +1,37 @@
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_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/services/error_handler_service.dart';
class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false;
bool isHistoryLoading = false;
bool isOfferDetailsLoading = false;
List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers'
String searchQuery = '';
OffersAndDiscountsRepo offersAndDiscountsRepo;
ErrorHandlerService errorHandlerService;
List<OffersAndDiscountsResponseModel> offersList = [];
List<OfferData> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = [];
OffersAndDiscountsDetailedResponseModel? offerDetails;
Meta? meta;
OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo,
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
bool get isRegionListLoading => isOffersLoading;
initOffersAndDiscounts() {
// Don't clear the list - keep dummy data until API returns
// Don't show loading state since we have dummy data
// offersList.clear();
// isOffersLoading = true;
offersList.clear();
isOffersLoading = true;
notifyListeners();
getOffersAndDiscounts();
}
@ -340,6 +57,9 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isOffersLoading = true;
notifyListeners();
final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
result.fold(
@ -347,15 +67,16 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
isOffersLoading = false;
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) {
onError(failure.toString());
}
},
(response) {
isOffersLoading = false;
if (response.data != null && response.data!.isNotEmpty) {
offersList = response.data!;
if (response.data != null) {
offersList = response.data!.data;
meta = response.data!.meta;
}
// If API returns empty or null, keep the dummy data
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 {
isHistoryLoading = true;
notifyListeners();
@ -396,25 +145,24 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
);
}
List<OffersAndDiscountsResponseModel> get filteredOffers {
List<OffersAndDiscountsResponseModel> filtered = offersList;
List<OfferData> get filteredOffers {
List<OfferData> filtered = offersList;
// 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'
);
// Check if offer's branches match any of the selected facility types
// You can customize this logic based on how you want to filter by facility
return true; // For now, show all when filtering is needed
}).toList();
}
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
return titleMatch || descriptionMatch;
final titleMatch = offer.name.toLowerCase().contains(searchQuery.toLowerCase());
final titleArMatch = offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
return titleMatch || titleArMatch;
}).toList();
}
@ -424,26 +172,14 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
List<OffersAndDiscountsResponseModel> get filteredHistory {
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
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
return titleMatch || descriptionMatch;
// Assuming history list uses the old structure - update when API is available
return true; // Placeholder for now
}).toList();
}
return filtered;
}
}

@ -1,53 +1,61 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:provider/provider.dart';
class OffersAndDiscountsDetailedPage extends StatelessWidget {
final OffersAndDiscountsResponseModel offer;
class OffersAndDiscountsDetailedPage extends StatefulWidget {
final int offerId;
const OffersAndDiscountsDetailedPage({
super.key,
required this.offer,
required this.offerId,
});
String _formatDateString(String? dateString) {
if (dateString == null || dateString.isEmpty) return '';
@override
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 {
DateTime date = DateTime.parse(dateString);
return DateFormat('d MMM, yyyy').format(date);
} catch (e) {
return dateString;
return '';
}
}
// 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 (isActive == true) {
return 'Active';
} else {
return 'Availed';
}
}
} catch (e) {
// Determine the status of the offer based on dates
String _getOfferStatus(DateTime validTo) {
DateTime now = DateTime.now();
if (now.isAfter(validTo)) {
return 'Expired';
} else {
return 'Active';
}
}
@ -55,8 +63,6 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
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:
@ -68,8 +74,6 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
switch (status) {
case 'Active':
return AppColors.successColor;
case 'Availed':
return AppColors.infoColor;
case 'Expired':
return AppColors.errorColor;
default:
@ -79,22 +83,57 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final status = _getOfferStatus(offer.endDate, offer.isActive);
appState = getIt.get<AppState>();
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}",
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Offer Image
if (offer.imageUrl != null)
Stack(
children: [
child: Consumer<OffersAndDiscountsViewModel>(
builder: (context, vm, child) {
if (vm.isOfferDetailsLoading) {
return Center(child: Utils.getLoadingWidget());
}
final offer = vm.offerDetails;
if (offer == null) {
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
}
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(
borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork(
url: offer.imageUrl!,
url: offer.imageUrl,
iconColor: AppColors.blackBgColor,
width: double.infinity,
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
if (offer.title != null)
offer.title!.toText24(
isBold: true,
fontWeight: FontWeight.w700,
),
// Title
(appState.getLanguageID() == 1 ? offer.nameAr : offer.name).toText24(
isBold: true,
fontWeight: FontWeight.w700,
),
SizedBox(height: 16.h),
SizedBox(height: 16.h),
// Chips Row (Valid Till and Status)
Wrap(
spacing: 12.w,
runSpacing: 12.h,
children: [
// Valid till chip
if (offer.endDate != null)
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.endDate)}",
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),
// Price Information
Row(
children: [
"Price: ".toText18(
isBold: true,
weight: FontWeight.w700,
),
"${offer.listPrice} ${offer.currency}".toText18(
color: AppColors.primaryRedColor,
isBold: true,
weight: FontWeight.w700,
),
],
),
SizedBox(height: 16.h),
// Chips Row (Valid Till and Status)
Wrap(
spacing: 12.w,
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
LocaleKeys.description.tr(context: context).toText18(
// Branches Section
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,
weight: FontWeight.w700,
),
SizedBox(height: 12.h),
// Description Content
if (offer.description != null)
offer.description!.toText16(
color: AppColors.greyTextColor,
height: 1.5,
),
SizedBox(height: 32.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),
SizedBox(height: 12.h),
// Description Content
(appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description).toText16(
color: AppColors.greyTextColor,
height: 1.5,
),
SizedBox(height: 24.h),
// Offer Lines Section
if (offer.offerLines.isNotEmpty) ...[
"Included Services".toText18(
isBold: true,
weight: FontWeight.w700,
),
SizedBox(height: 12.h),
...offer.offerLines.map((line) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(12.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(appState.getLanguageID() == 1 ? line.nameAr : line.name).toText16(
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:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class OffersAndDiscountsHistoryPage extends StatefulWidget {
@ -34,69 +25,11 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
@override
void initState() {
scheduleMicrotask(() {
offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
// offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
});
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
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
@ -109,179 +42,37 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
offersAndDiscountVM.isHistoryLoading
? Container(
height: 200.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
// Temporarily show a message that history is not available yet
Container(
padding: EdgeInsets.all(24.h),
child: Column(
children: [
Icon(
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),
],
);

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

Loading…
Cancel
Save