Offers And Discounts Apis

offers_discounts
Aamir Muhammad 1 week ago
parent 1d9783a5b3
commit 369a566006

@ -1958,5 +1958,6 @@
"invoiceHistory": "سجل الفواتير", "invoiceHistory": "سجل الفواتير",
"refund": "استرداد", "refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:" "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"paymentSuccessful": "تم الدفع بنجاح"
} }

@ -1947,7 +1947,8 @@
"amountOnly": "Amount", "amountOnly": "Amount",
"invoiceHistory": "Invoice History", "invoiceHistory": "Invoice History",
"parkingQR": "Parking QR Code", "parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:" "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"paymentSuccessful": "Payment SuccessFul"
} }

@ -304,6 +304,7 @@ class ApiConsts {
static final pomsBaseUrl = 'https://poms-uat.odoo.com'; static final pomsBaseUrl = 'https://poms-uat.odoo.com';
static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers'; static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers'; static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetOffersAndDiscountPurchasedHistory = '$pomsBaseUrl/api/v1/mobile/my/offers';
// ************ static values for Api **************** // ************ static values for Api ****************
static final double appVersionID = 21.3; static final double appVersionID = 21.3;

@ -297,7 +297,8 @@ enum ServiceTypeEnum {
appointmentPayment, //2 appointmentPayment, //2
covidPayment, //2 covidPayment, //2
erOnlineCheckIn, //3 erOnlineCheckIn, //3
liveCareAppointment //4 liveCareAppointment, //4
offerPayment //5
} }
extension ServiceTypeEnumExt on ServiceTypeEnum { extension ServiceTypeEnumExt on ServiceTypeEnum {
@ -315,6 +316,8 @@ extension ServiceTypeEnumExt on ServiceTypeEnum {
return "ER Online Check In"; return "ER Online Check In";
case ServiceTypeEnum.liveCareAppointment: case ServiceTypeEnum.liveCareAppointment:
return "LiveCare Appointment"; return "LiveCare Appointment";
case ServiceTypeEnum.offerPayment:
return "Offer Payment";
} }
} }
@ -332,6 +335,8 @@ extension ServiceTypeEnumExt on ServiceTypeEnum {
return 3; return 3;
case ServiceTypeEnum.liveCareAppointment: case ServiceTypeEnum.liveCareAppointment:
return 4; return 4;
case ServiceTypeEnum.offerPayment:
return 5;
} }
} }
} }

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

@ -68,10 +68,7 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
@override @override
Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId) async { Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId) async {
Map<String, String> headers = { Map<String, String> headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey};
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
try { try {
GenericApiModel<OffersAndDiscountsDetailedResponseModel>? apiResponse; GenericApiModel<OffersAndDiscountsDetailedResponseModel>? apiResponse;
@ -119,7 +116,7 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse; GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
Failure? failure; Failure? failure;
await apiClient.post( await apiClient.post(
GET_OFFERS_AND_DISCOUNTS_HISTORY, // TODO: Replace with actual API endpoint ApiConsts.pomsGetOffersAndDiscountPurchasedHistory,
body: mapDevice, body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) { onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType; failure = failureType;

@ -1,6 +1,6 @@
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/models/offers_and_discounts_detailed_response_model.dart' hide Category;
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -8,7 +8,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false; bool isOffersLoading = false;
bool isHistoryLoading = false; bool isHistoryLoading = false;
bool isOfferDetailsLoading = false; bool isOfferDetailsLoading = false;
List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers' List<int> selectedCategories = []; // Empty means 'All Offers'
String searchQuery = ''; String searchQuery = '';
OffersAndDiscountsRepo offersAndDiscountsRepo; OffersAndDiscountsRepo offersAndDiscountsRepo;
@ -17,14 +17,13 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
List<OfferData> offersList = []; List<OfferData> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = []; List<OffersAndDiscountsResponseModel> historyList = [];
OffersAndDiscountsDetailedResponseModel? offerDetails; OffersAndDiscountsDetailedResponseModel? offerDetails;
OffersAndDiscountsDetailedResponseModel? selectedOffer;
Meta? meta; Meta? meta;
OffersAndDiscountsViewModel({ OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo, required this.offersAndDiscountsRepo,
required this.errorHandlerService, required this.errorHandlerService,
}) { }) {}
}
// For loading state compatibility with existing code // For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading; bool get isRegionListLoading => isOffersLoading;
@ -46,8 +45,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
setSelectedFacility(List<String> facilities) { setSelectedCategories(List<int> categoryIds) {
selectedFacilities = facilities; selectedCategories = categoryIds;
notifyListeners(); notifyListeners();
} }
@ -56,6 +55,25 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Get unique categories from the offers list
List<Category> getUniqueCategories() {
if (offersList.isEmpty) return [];
final Map<int, Category> categoriesMap = {};
for (var offer in offersList) {
if (!categoriesMap.containsKey(offer.category.id)) {
categoriesMap[offer.category.id] = offer.category;
}
}
return categoriesMap.values.toList();
}
setSelectedOffer(OffersAndDiscountsDetailedResponseModel offer) {
selectedOffer = offer;
notifyListeners();
}
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isOffersLoading = true; isOffersLoading = true;
notifyListeners(); notifyListeners();
@ -148,12 +166,10 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
List<OfferData> get filteredOffers { List<OfferData> get filteredOffers {
List<OfferData> filtered = offersList; List<OfferData> filtered = offersList;
// Filter by facility types (multi-select support) // Filter by categories (if any categories are selected)
if (!selectedFacilities.contains('All Offers')) { if (selectedCategories.isNotEmpty) {
filtered = filtered.where((offer) { filtered = filtered.where((offer) {
// Check if offer's branches match any of the selected facility types return selectedCategories.contains(offer.category.id);
// 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(); }).toList();
} }

@ -1940,5 +1940,6 @@ abstract class LocaleKeys {
static const refund = 'refund'; static const refund = 'refund';
static const parkingQR = 'parkingQR'; static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc'; static const parkingQRDesc = 'parkingQRDesc';
static const paymentSuccessful = 'paymentSuccessful';
} }

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.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';
@ -5,17 +6,22 @@ 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/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget { class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
final List<String>? images; final int offerId;
final int initialIndex; final int initialIndex;
const OfferAndDiscountsFullScreenSwiperPage({ const OfferAndDiscountsFullScreenSwiperPage({
super.key, super.key,
this.images, required this.offerId,
this.initialIndex = 0, this.initialIndex = 0,
}); });
@ -26,21 +32,48 @@ class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscountsFullScreenSwiperPage> { class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscountsFullScreenSwiperPage> {
late PageController _pageController; late PageController _pageController;
late int _currentPage; late int _currentPage;
late List<String> _promoImages; late OffersAndDiscountsViewModel viewModel;
late AppState appState;
List<String> _promoImages = []; // Changed from late to regular variable
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Use provided images or default demo images
_promoImages = widget.images ??
[
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
];
_currentPage = widget.initialIndex; _currentPage = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex); _pageController = PageController(initialPage: widget.initialIndex);
// Fetch offer details
scheduleMicrotask(() {
viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.getOfferDetails(widget.offerId);
});
}
// Collect all images from the offer details
List<String> _getAllImageUrls(OffersAndDiscountsDetailedResponseModel offer) {
List<String> allImages = [];
// Add main images
if (offer.images.main.isNotEmpty) {
allImages.addAll(offer.images.main.map((banner) => banner.url));
}
// Add banner images
if (offer.images.banners.isNotEmpty) {
allImages.addAll(offer.images.banners.map((banner) => banner.url));
}
// Add detail images
if (offer.images.details.isNotEmpty) {
allImages.addAll(offer.images.details.map((banner) => banner.url));
}
// If no images from the images object, use the main imageUrl
if (allImages.isEmpty && offer.imageUrl.isNotEmpty) {
allImages.add(offer.imageUrl);
}
return allImages;
} }
@override @override
@ -51,66 +84,178 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
appState = getIt.get<AppState>();
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFADA6D0), backgroundColor: const Color(0xFFADA6D0),
body: SafeArea( body: Consumer<OffersAndDiscountsViewModel>(
child: Stack( builder: (context, vm, child) {
children: [ // Show loading state
// Main content area if (vm.isOfferDetailsLoading) {
Column( return SafeArea(
children: [ child: Stack(
SizedBox(height: 26.h), children: [
Center(child: Utils.getLoadingWidget()),
// Page indicators // Close button while loading
_buildPageIndicators(), Positioned(
top: 26.h + 14.h,
SizedBox(height: 70.h), right: 24.w,
child: _buildCloseButton(),
// Swipeable image section ),
Expanded( ],
child: PageView.builder( ),
controller: _pageController, );
onPageChanged: (index) { }
setState(() {
_currentPage = index; final offer = vm.offerDetails;
}); if (offer == null) {
}, return SafeArea(
itemCount: _promoImages.length, child: Stack(
itemBuilder: (context, index) { children: [
return Center( Center(
child: Padding( child: Text(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h), 'No offer details available',
child: Transform.flip( style: TextStyle(
flipX: getIt.get<AppState>().isArabic(), fontSize: 16.f,
child: ClipRRect( color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(16.r), ),
child: Utils.buildImgWithAssets( ),
icon: _promoImages[index], ),
fit: BoxFit.contain, Positioned(
), top: 26.h + 14.h,
), right: 24.w,
child: _buildCloseButton(),
),
],
),
);
}
// Get all images from the offer
_promoImages = _getAllImageUrls(offer);
if (_promoImages.isEmpty) {
return SafeArea(
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_not_supported,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
Text(
'No images available',
style: TextStyle(
fontSize: 16.f,
color: AppColors.whiteColor,
), ),
), ),
); ],
}, ),
), ),
), Positioned(
top: 26.h + 14.h,
right: 24.w,
child: _buildCloseButton(),
),
],
),
);
}
return SafeArea(
child: Stack(
children: [
// Main content area
Column(
children: [
SizedBox(height: 26.h),
// Page indicators
_buildPageIndicators(),
SizedBox(height: 70.h), SizedBox(height: 70.h),
// Bottom action buttons
_buildBottomActions(),
SizedBox(height: 12.h), // Swipeable image section
Expanded(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemCount: _promoImages.length,
itemBuilder: (context, index) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h),
child: Transform.flip(
flipX: appState.isArabic(),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Utils.buildImgWithNetwork(
url: _promoImages[index],
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
iconColor: AppColors.whiteColor,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_not_supported,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
Text(
'Failed to load image',
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
),
);
},
),
),
),
),
);
},
),
),
SizedBox(height: 70.h),
// Bottom action buttons
_buildBottomActions(vm),
SizedBox(height: 12.h),
],
),
// Close button (top right)
Positioned(
top: 26.h + 14.h, // After indicator spacing
right: 24.w,
child: _buildCloseButton()),
], ],
), ),
);
// Close button (top right) },
Positioned(
top: 26.h + 14.h, // After indicator spacing
right: 24.w,
child: _buildCloseButton()),
],
),
), ),
); );
} }
@ -162,7 +307,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
); );
} }
Widget _buildBottomActions() { Widget _buildBottomActions(OffersAndDiscountsViewModel vm) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w), padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row( child: Row(
@ -174,7 +319,15 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { onPressed: () {
// Handle buy now action // Set the selected offer and navigate to payment page
if (vm.offerDetails != null) {
vm.setSelectedOffer(vm.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
}
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor,
@ -206,6 +359,7 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
_buildIconButton( _buildIconButton(
svgIcon: AppAssets.share, svgIcon: AppAssets.share,
onTap: () async { onTap: () async {
// ignore: deprecated_member_use
await Share.share('Check out this amazing offer!'); await Share.share('Check out this amazing offer!');
}), }),
], ],

@ -0,0 +1,691 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.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/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:smooth_corner/smooth_corner.dart';
class OffersAndDiscountPaymentPage extends StatefulWidget {
OffersAndDiscountPaymentPage({super.key});
@override
State<OffersAndDiscountPaymentPage> createState() => _OffersAndDiscountPaymentPageState();
}
class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentPage> {
late PayfortViewModel payfortViewModel;
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
late MyAppointmentsViewModel myAppointmentsViewModel;
late AppState appState;
late PayTabsViewModel paytabsViewModel;
MyInAppBrowser? browser;
String selectedPaymentMethod = "";
String transID = "";
bool isShowTamara = false;
String tamaraPaymentStatus = "";
String tamaraOrderID = "";
@override
void initState() {
scheduleMicrotask(() {
payfortViewModel.initPayfortViewModel();
});
super.initState();
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
payfortViewModel = Provider.of<PayfortViewModel>(context, listen: false);
paytabsViewModel = Provider.of<PayTabsViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersVM, child) {
final selectedOffer = offersVM.selectedOffer;
if (selectedOffer == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64.h, color: AppColors.errorColor),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText16(),
],
),
);
}
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.payment.tr(context: context)}",
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Offer Summary Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(appState.getLanguageID() == 1 ? selectedOffer.nameAr : selectedOffer.name).toText18(isBold: true),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.price.tr(context: context).toText14(color: AppColors.greyTextColor),
"${selectedOffer.listPrice} ${selectedOffer.currency}".toText16(isBold: true, color: AppColors.primaryRedColor),
],
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
LocaleKeys.selectPaymentMethod.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
),
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.listPrice,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("mada", selectedOffer.listPrice);
}
}
}),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
),
SizedBox(height: 16.h),
LocaleKeys.visaOrMastercard.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
),
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.listPrice,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("visa", selectedOffer.listPrice);
}
}
}),
],
),
),
),
),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: Consumer<PayfortViewModel>(builder: (context, payfortVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
LocaleKeys.totalAmountToPay.tr().toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 17.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.amountBeforeTax.tr().toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.vatExclusivePrice.toString().toText16(isBold: true), AppColors.blackColor, 13,
isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.vat15.tr().toText14(isBold: true, color: AppColors.greyTextColor),
Utils.getPaymentAmountWithSymbol(
(selectedOffer.vatInclusivePrice - selectedOffer.vatExclusivePrice).toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13,
isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 17.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17,
isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
Platform.isIOS
? Utils.buildSvgWithAssets(
icon: AppAssets.apple_pay_button,
width: 200.h,
height: 80.h,
fit: BoxFit.contain,
applyThemeColor: false
).paddingSymmetrical(24.h, 0.h).onPress(() {
if (Utils.havePrivilege(103)) {
startApplePay(selectedOffer.listPrice);
} else {
openPaymentURL("ApplePay", selectedOffer.listPrice);
}
})
: SizedBox(height: 12.h),
SizedBox(height: 12.h),
],
);
}),
),
],
);
}),
);
}
onBrowserLoadStart(String url) {
print("onBrowserLoadStart");
print(url);
if (selectedPaymentMethod == "TAMARA") {
if (Platform.isAndroid) {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['status']!;
tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!;
} else {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['paymentStatus']!;
tamaraOrderID = uri.queryParameters['orderId']!;
}
}
// if(selectedPaymentMethod != "TAMARA") {
MyInAppBrowser.successURLS.forEach((element) {
if (url.contains(element)) {
browser?.close();
MyInAppBrowser.isPaymentDone = true;
return;
}
});
// }
// if(selectedPaymentMethod != "TAMARA") {
MyInAppBrowser.errorURLS.forEach((element) {
if (url.contains(element)) {
browser?.close();
MyInAppBrowser.isPaymentDone = false;
return;
}
});
// }
}
onBrowserExit(bool isPaymentMade) async {
debugPrint("onBrowserExit Called!!!!");
checkPaymentStatus();
}
void checkPaymentStatus() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
if (selectedPaymentMethod == "TAMARA") {
await payfortViewModel.checkTamaraPaymentStatus(
transactionID: transID,
onSuccess: (apiResponse) async {
if (apiResponse.data["status"].toString().toLowerCase() == "success") {
tamaraOrderID = apiResponse.data["tamara_order_id"].toString();
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
await payfortViewModel.checkPaymentStatus(
transactionID: transID,
onSuccess: (apiResponse) async {
debugPrint(apiResponse.data.toString());
if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") {
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
});
}
}
openPaymentURL(String paymentMethod, double amount) {
browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context);
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
if (selectedOffer == null) return;
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
browser?.openPaymentBrowser(
amount,
"Offer Payment",
transID,
"12",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
selectedPaymentMethod,
appState.getAuthenticatedUser()!.patientType.toString(),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!,
browser!,
false,
"4",
selectedOffer.id.toString(),
context,
"3");
}
startApplePay(double amount) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingApplePayDetails.tr());
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
if (selectedOffer == null) {
LoaderBottomSheet.hideLoader();
return;
}
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 2);
applePayInsertRequest.clientRequestID = transID;
applePayInsertRequest.clinicID = selectedOffer.id;
applePayInsertRequest.currency = selectedOffer.currency;
applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com";
applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString();
applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}";
applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken);
applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken);
applePayInsertRequest.doctorID = 0;
applePayInsertRequest.projectID = "12";
applePayInsertRequest.serviceID = ServiceTypeEnum.offerPayment.getIdFromServiceEnum().toString();
applePayInsertRequest.channelID = 3;
applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString();
applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType;
applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa;
applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(DateTime.now());
applePayInsertRequest.appointmentNo = 0;
applePayInsertRequest.orderDescription = "Offer Payment";
applePayInsertRequest.liveServiceID = selectedOffer.id.toString();
applePayInsertRequest.latitude = "0.0";
applePayInsertRequest.longitude = "0.0";
applePayInsertRequest.amount = amount.toString();
applePayInsertRequest.isSchedule = "0";
applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en';
applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2;
applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId;
applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html";
applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html";
applePayInsertRequest.paymentOption = "ApplePay";
applePayInsertRequest.isMobSDK = true;
applePayInsertRequest.merchantReference = transID;
applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier;
applePayInsertRequest.commandType = "PURCHASE";
applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature;
applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode;
applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest;
applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse;
applePayInsertRequest.returnURL = "";
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Offer Payment", amount);
paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
payfortViewModel.paymentWithApplePay(
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
orderDescription: "Offer Payment",
orderAmount: amount,
merchantReference: transID,
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier,
applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode,
applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest,
currency: selectedOffer.currency,
onFailed: (failureResult) async {
log("failureResult: ${failureResult.message.toString()}");
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: failureResult.message.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onSucceeded: (successResult) async {
LoaderBottomSheet.hideLoader();
log("successResult: ${successResult.responseMessage.toString()}");
selectedPaymentMethod = successResult.paymentOption ?? "VISA";
checkPaymentStatus();
},
);
}
});
}
}

@ -2,6 +2,7 @@ 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_widget_from_html/flutter_widget_from_html.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/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -11,19 +12,18 @@ 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/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_discount_payment_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class OffersAndDiscountsDetailedPage extends StatefulWidget { class OffersAndDiscountsDetailedPage extends StatefulWidget {
final int offerId; final int offerId;
const OffersAndDiscountsDetailedPage({ const OffersAndDiscountsDetailedPage({super.key, required this.offerId});
super.key,
required this.offerId,
});
@override @override
State<OffersAndDiscountsDetailedPage> createState() => _OffersAndDiscountsDetailedPageState(); State<OffersAndDiscountsDetailedPage> createState() => _OffersAndDiscountsDetailedPageState();
@ -99,6 +99,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context))); return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
} }
print(offer.toJson());
final status = _getOfferStatus(offer.validTo); final status = _getOfferStatus(offer.validTo);
return SingleChildScrollView( return SingleChildScrollView(
@ -156,20 +157,14 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
SizedBox(height: 24.h), SizedBox(height: 24.h),
// Title // Title
(appState.getLanguageID() == 1 ? offer.nameAr : offer.name).toText24( (appState.getLanguageID() == 1 ? offer.nameAr : offer.name).toText24(isBold: true, fontWeight: FontWeight.w700),
isBold: true,
fontWeight: FontWeight.w700,
),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Price Information // Price Information
Row( Row(
children: [ children: [
"Price: ".toText18( "Price: ".toText18(isBold: true, weight: FontWeight.w700),
isBold: true,
weight: FontWeight.w700,
),
"${offer.listPrice} ${offer.currency}".toText18( "${offer.listPrice} ${offer.currency}".toText18(
color: AppColors.primaryRedColor, color: AppColors.primaryRedColor,
isBold: true, isBold: true,
@ -234,18 +229,18 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
], ],
// Description Heading // Description Heading
LocaleKeys.description.tr(context: context).toText18( LocaleKeys.description.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
isBold: true,
weight: FontWeight.w700,
),
SizedBox(height: 12.h), SizedBox(height: 12.h),
// Description Content // Description Content
(appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description).toText16( HtmlWidget(
color: AppColors.greyTextColor, (appState.getLanguageID() == 1 ? offer.descriptionAr : offer.description),
height: 1.5,
), ),
//.toText16(
// color: AppColors.greyTextColor,
// height: 1.5,
// )
SizedBox(height: 24.h), SizedBox(height: 24.h),
@ -299,7 +294,13 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { onPressed: () {
// Handle buy now action // Set the selected offer and navigate to payment page
viewModel.setSelectedOffer(offer);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor,

@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.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/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_discount_payment_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_history_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_history_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart';
@ -19,8 +20,10 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import '../../widgets/input_widget.dart'; import '../../widgets/input_widget.dart';
@ -50,6 +53,85 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
super.dispose(); super.dispose();
} }
Widget _buildShimmerCard() {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image placeholder
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
),
),
),
// Content placeholder
Expanded(
flex: 3,
child: Padding(
padding: EdgeInsets.all(12.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Title placeholder (2 lines)
Container(
width: double.infinity,
height: 14.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 6.h),
Container(
width: 100.w,
height: 14.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 8.h),
// Chip placeholder
Container(
width: 120.w,
height: 24.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
),
),
SizedBox(height: 20.h),
// Button placeholder
Container(
width: double.infinity,
height: 40.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
),
),
],
),
),
),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -68,6 +150,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) { child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
return Column( return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Search Input // Search Input
@ -92,10 +175,13 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
OffersAndDiscountTypeSelectionWidget( OffersAndDiscountTypeSelectionWidget(
selectedOffers: offersAndDiscountVM.selectedFacilities, selectedCategories: offersAndDiscountVM.selectedCategories,
onOfferClicked: (selectedValues) { onCategoryClicked: (selectedCategoryIds) {
offersAndDiscountVM.setSelectedFacility(selectedValues); offersAndDiscountVM.setSelectedCategories(selectedCategoryIds);
}, },
categories: offersAndDiscountVM.getUniqueCategories(),
appState: appState,
isLoading: offersAndDiscountVM.isOffersLoading,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -103,7 +189,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
// Offers Grid // Offers Grid
offersAndDiscountVM.isOffersLoading offersAndDiscountVM.isOffersLoading
? GridView.builder( ? GridView.builder(
padding: EdgeInsets.symmetric(horizontal: 24.w), padding: EdgeInsets.symmetric(horizontal: 0.w),
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@ -112,17 +198,10 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
mainAxisSpacing: 16.h, mainAxisSpacing: 16.h,
childAspectRatio: 0.55, // Adjust this to control card height childAspectRatio: 0.55, // Adjust this to control card height
), ),
itemCount: 6, itemCount: 4,
// Show 6 loading placeholders // Show 4 loading placeholders
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( return _buildShimmerCard();
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Center(child: Utils.getLoadingWidget()),
);
}, },
) )
: offersAndDiscountVM.filteredOffers.isNotEmpty : offersAndDiscountVM.filteredOffers.isNotEmpty
@ -193,9 +272,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
appState.getLanguageID() == 1 appState.getLanguageID() == 1 ? offersAndDiscountVM.filteredOffers[index].nameAr : offersAndDiscountVM.filteredOffers[index].name,
? offersAndDiscountVM.filteredOffers[index].nameAr
: offersAndDiscountVM.filteredOffers[index].name,
style: TextStyle( style: TextStyle(
fontSize: 14.f, fontSize: 14.f,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@ -221,13 +298,24 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
text: 'Buy Now', text: 'Buy Now',
icon: AppAssets.shoppingCart, icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { onPressed: () async {
Navigator.of(context).push( LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
CustomPageRoute( await offersAndDiscountVM.getOfferDetails(
page: OffersAndDiscountsDetailedPage( offersAndDiscountVM.filteredOffers[index].id,
offerId: offersAndDiscountVM.filteredOffers[index].id, onSuccess: (response) {
), LoaderBottomSheet.hideLoader();
), if (offersAndDiscountVM.offerDetails != null) {
offersAndDiscountVM.setSelectedOffer(offersAndDiscountVM.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
}
},
onError: (error) {
LoaderBottomSheet.hideLoader();
},
); );
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
@ -262,3 +350,11 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
); );
} }
} }
// Navigator.of(context).push(
// CustomPageRoute(
// page: OffersAndDiscountsDetailedPage(
// offerId: offersAndDiscountVM.filteredOffers[index].id,
// ),
// ),
// );

@ -1,68 +1,97 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/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/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:shimmer/shimmer.dart';
class OffersAndDiscountTypeSelectionWidget extends StatelessWidget { class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
final List<String> selectedOffers; final List<int> selectedCategories;
final Function(List<String>) onOfferClicked; final Function(List<int>) onCategoryClicked;
final List<Category> categories;
final AppState appState;
final bool isLoading;
const OffersAndDiscountTypeSelectionWidget({ const OffersAndDiscountTypeSelectionWidget({
super.key, super.key,
required this.selectedOffers, required this.selectedCategories,
required this.onOfferClicked, required this.onCategoryClicked,
required this.categories,
required this.appState,
this.isLoading = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isArabic = appState.getLanguageID() == 1;
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: [ children: [
_buildOfferTypeCard( // Show loading skeleton chips
context: context, if (isLoading) ...[
title: "All Offers", _buildLoadingChip(width: 80.w),
facilityType: 'All Offers', SizedBox(width: 12.w),
isSelected: selectedOffers.contains('All Offers'), _buildLoadingChip(width: 80.w),
), SizedBox(width: 12.w),
SizedBox(width: 12.w), _buildLoadingChip(width: 120.w),
_buildOfferTypeCard( SizedBox(width: 12.w),
context: context, _buildLoadingChip(width: 95.w),
title: 'Female', SizedBox(width: 12.w),
facilityType: 'Female', _buildLoadingChip(width: 110.w),
isSelected: selectedOffers.contains('Female'), ]
), // Show actual category chips
SizedBox(width: 12.w), else ...[
_buildOfferTypeCard( // "All Offers" button
context: context, _buildOfferTypeCard(
title: 'OB-Gyne', context: context,
facilityType: 'OB-Gyne', title: isArabic ? "جميع العروض" : "All Offers",
isSelected: selectedOffers.contains('OB-Gyne'), categoryId: null, // null represents "All Offers"
), isSelected: selectedCategories.isEmpty,
SizedBox(width: 12.w), ),
_buildOfferTypeCard( SizedBox(width: 12.w),
context: context,
title: 'Dermatology', // Dynamic category buttons
facilityType: 'Dermatology', ...categories
isSelected: selectedOffers.contains('Dermatology'), .map((category) => Padding(
), padding: EdgeInsets.only(right: 12.w),
SizedBox(width: 12.w), child: _buildOfferTypeCard(
_buildOfferTypeCard( context: context,
context: context, title: isArabic ? category.nameAr : category.name,
title: 'Radiology', categoryId: category.id,
facilityType: 'Radiology', isSelected: selectedCategories.contains(category.id),
isSelected: selectedOffers.contains('Radiology'), ),
), ))
.toList(),
],
], ],
), ),
); );
} }
Widget _buildLoadingChip({required double width}) {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
width: width,
height: 40.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: false,
),
),
);
}
Widget _buildOfferTypeCard({ Widget _buildOfferTypeCard({
required BuildContext context, required BuildContext context,
required String title, required String title,
required String facilityType, required int? categoryId, // null means "All Offers"
required bool isSelected, required bool isSelected,
}) { }) {
return AnimatedContainer( return AnimatedContainer(
@ -82,32 +111,29 @@ class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
), ),
), ),
).onPress(() { ).onPress(() {
List<String> updatedSelection = List.from(selectedOffers); List<int> updatedSelection = List.from(selectedCategories);
if (facilityType == 'All Offers') { if (categoryId == null) {
if (updatedSelection.contains('All Offers')) { // "All Offers" clicked
if (updatedSelection.isEmpty) {
// Already showing all, do nothing or toggle to empty
updatedSelection.clear(); updatedSelection.clear();
} else { } else {
// Clear all selections to show all offers
updatedSelection.clear(); updatedSelection.clear();
updatedSelection.add('All Offers');
} }
} else { } else {
// Remove "All Offers" if any specific type is selected // Specific category clicked
updatedSelection.remove('All Offers'); // Toggle the clicked category
if (updatedSelection.contains(categoryId)) {
// Toggle the clicked facility type updatedSelection.remove(categoryId);
if (updatedSelection.contains(facilityType)) { // If no selection left, it means show all offers (empty list)
updatedSelection.remove(facilityType);
// If no selection left, default to "All Offers"
if (updatedSelection.isEmpty) {
updatedSelection.add('All Offers');
}
} else { } else {
updatedSelection.add(facilityType); updatedSelection.add(categoryId);
} }
} }
onOfferClicked(updatedSelection); onCategoryClicked(updatedSelection);
}); });
} }
} }

@ -1,8 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.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/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart'; import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_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/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import 'package:smooth_corner/smooth_corner.dart'; import 'package:smooth_corner/smooth_corner.dart';
import '../../../core/utils/utils.dart'; import '../../../core/utils/utils.dart';
@ -17,21 +24,17 @@ class OffersAndDiscountsCarousel extends StatefulWidget {
class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel> { class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
Timer? _autoScrollTimer; Timer? _autoScrollTimer;
late AppState appState;
// List of offer images - you can add more images here
final List<String> offerImages = [
'assets/images/offersanddiscounts/img1.png',
'assets/images/offersanddiscounts/img2.png',
'assets/images/offersanddiscounts/img3.png',
'assets/images/offersanddiscounts/img1.png',
'assets/images/offersanddiscounts/img2.png',
'assets/images/offersanddiscounts/img3.png',
'assets/images/offersanddiscounts/img1.png',
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
appState = getIt.get<AppState>();
// Initialize the view model and load offers data
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.initOffersAndDiscounts();
});
// _startAutoScroll(); // _startAutoScroll();
} }
@ -65,23 +68,77 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<OffersAndDiscountsViewModel>(
builder: (context, viewModel, child) {
// Show skeleton loader while loading
if (viewModel.isOffersLoading) {
return _buildSkeletonLoader();
}
// Show empty state if no offers
if (viewModel.offersList.isEmpty) {
return SizedBox(
height: 79.h,
child: Center(
child: Text(
'No offers available',
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
),
);
}
// Display offers
return SizedBox(
height: 79.h,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 0.w),
itemCount: viewModel.offersList.length,
itemBuilder: (context, index) {
final offer = viewModel.offersList[index];
return _buildOfferItem(offer.imageUrl).onPress((){
Navigator.push(
context,
CustomPageRoute(
page: OfferAndDiscountsFullScreenSwiperPage(offerId: offer.id),
),
);
});
},
separatorBuilder: (context, index) => SizedBox(width: 12.w),
),
);
},
);
}
Widget _buildSkeletonLoader() {
return SizedBox( return SizedBox(
height: 79.h, height: 79.h,
child: ListView.separated( child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 0.w), padding: EdgeInsets.symmetric(horizontal: 0.w),
itemCount: offerImages.length * 100, itemCount: 5, // Show 5 skeleton items
itemBuilder: (context, index) { itemBuilder: (context, index) {
final imageIndex = index % offerImages.length; return Shimmer.fromColors(
return GestureDetector( baseColor: AppColors.greyColor.withValues(alpha: 0.3),
onTap: () { highlightColor: AppColors.whiteColor.withValues(alpha: 0.5),
Navigator.push( child: Container(
context, width: 79.w,
MaterialPageRoute(builder: (context) => const OfferAndDiscountsFullScreenSwiperPage()), height: 79.h,
); decoration: ShapeDecoration(
}, color: AppColors.greyColor,
child: _buildOfferItem(offerImages[imageIndex]), shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(13.r),
smoothness: 0.6,
),
),
),
); );
}, },
separatorBuilder: (context, index) => SizedBox(width: 12.w), separatorBuilder: (context, index) => SizedBox(width: 12.w),
@ -89,7 +146,7 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
); );
} }
Widget _buildOfferItem(String imagePath) { Widget _buildOfferItem(String imageUrl) {
return Container( return Container(
width: 79.w, width: 79.w,
height: 79.h, height: 79.h,
@ -114,10 +171,30 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2.0),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(11.r), borderRadius: BorderRadius.circular(11.r),
child: Utils.buildImgWithAssets( child: imageUrl.isNotEmpty
icon: imagePath, ? Utils.buildImgWithNetwork(
fit: BoxFit.cover, url: imageUrl,
), fit: BoxFit.cover,
iconColor: AppColors.blackBgColor,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Icon(
Icons.image_not_supported,
size: 24.h,
color: AppColors.greyTextColor,
),
);
},
)
: Container(
color: AppColors.greyColor,
child: Icon(
Icons.local_offer,
size: 24.h,
color: AppColors.greyTextColor,
),
),
), ),
), ),
), ),

Loading…
Cancel
Save