diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index d39e8bd7..d45804d3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1958,5 +1958,6 @@ "invoiceHistory": "سجل الفواتير", "refund": "استرداد", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", - "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:" + "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:", + "paymentSuccessful": "تم الدفع بنجاح" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 61a6da03..e1702e4b 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1947,7 +1947,8 @@ "amountOnly": "Amount", "invoiceHistory": "Invoice History", "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" } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8b0571a9..faf5fcb3 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -304,6 +304,7 @@ class ApiConsts { 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 final pomsGetOffersAndDiscountPurchasedHistory = '$pomsBaseUrl/api/v1/mobile/my/offers'; // ************ static values for Api **************** static final double appVersionID = 21.3; diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 4ac919e7..6fa2e11b 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -297,7 +297,8 @@ enum ServiceTypeEnum { appointmentPayment, //2 covidPayment, //2 erOnlineCheckIn, //3 - liveCareAppointment //4 + liveCareAppointment, //4 + offerPayment //5 } extension ServiceTypeEnumExt on ServiceTypeEnum { @@ -315,6 +316,8 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { return "ER Online Check In"; case ServiceTypeEnum.liveCareAppointment: return "LiveCare Appointment"; + case ServiceTypeEnum.offerPayment: + return "Offer Payment"; } } @@ -332,6 +335,8 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { return 3; case ServiceTypeEnum.liveCareAppointment: return 4; + case ServiceTypeEnum.offerPayment: + return 5; } } } diff --git a/lib/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart b/lib/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart index a65df595..ed70ca77 100644 --- a/lib/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart +++ b/lib/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart @@ -14,7 +14,7 @@ class OffersAndDiscountsDetailedResponseModel { DateTime validTo; String description; String descriptionAr; - int vatExclusivePrice; + double vatExclusivePrice; double vatInclusivePrice; bool requirePatientRegistration; Images images; @@ -46,46 +46,46 @@ class OffersAndDiscountsDetailedResponseModel { String toRawJson() => json.encode(toJson()); factory OffersAndDiscountsDetailedResponseModel.fromJson(Map json) => OffersAndDiscountsDetailedResponseModel( - id: json["id"], - ref: json["ref"], - name: json["name"], - nameAr: json["name_ar"], - category: Category.fromJson(json["category"]), - branches: List.from(json["branches"].map((x) => Branch.fromJson(x))), - 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.from(json["offer_lines"].map((x) => OfferLine.fromJson(x))), - ); + id: json["id"], + ref: json["ref"], + name: json["name"], + nameAr: json["name_ar"], + category: Category.fromJson(json["category"]), + branches: List.from(json["branches"].map((x) => Branch.fromJson(x))), + 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.from(json["offer_lines"].map((x) => OfferLine.fromJson(x))), + ); Map toJson() => { - "id": id, - "ref": ref, - "name": name, - "name_ar": nameAr, - "category": category.toJson(), - "branches": List.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.from(offerLines.map((x) => x.toJson())), - }; + "id": id, + "ref": ref, + "name": name, + "name_ar": nameAr, + "category": category.toJson(), + "branches": List.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.from(offerLines.map((x) => x.toJson())), + }; } class Branch { @@ -106,18 +106,18 @@ class Branch { String toRawJson() => json.encode(toJson()); factory Branch.fromJson(Map json) => Branch( - id: json["id"], - code: json["code"], - name: json["name"], - nameAr: json["name_ar"], - ); + id: json["id"], + code: json["code"], + name: json["name"], + nameAr: json["name_ar"], + ); Map toJson() => { - "id": id, - "code": code, - "name": name, - "name_ar": nameAr, - }; + "id": id, + "code": code, + "name": name, + "name_ar": nameAr, + }; } class Category { @@ -138,18 +138,18 @@ class Category { String toRawJson() => json.encode(toJson()); factory Category.fromJson(Map json) => Category( - id: json["id"], - code: json["code"], - name: json["name"], - nameAr: json["name_ar"], - ); + id: json["id"], + code: json["code"], + name: json["name"], + nameAr: json["name_ar"], + ); Map toJson() => { - "id": id, - "code": code, - "name": name, - "name_ar": nameAr, - }; + "id": id, + "code": code, + "name": name, + "name_ar": nameAr, + }; } class Images { @@ -168,16 +168,16 @@ class Images { String toRawJson() => json.encode(toJson()); factory Images.fromJson(Map json) => Images( - main: List.from(json["main"].map((x) => Banner.fromJson(x))), - banners: List.from(json["banners"].map((x) => Banner.fromJson(x))), - details: List.from(json["details"].map((x) => Banner.fromJson(x))), - ); + main: List.from(json["main"].map((x) => Banner.fromJson(x))), + banners: List.from(json["banners"].map((x) => Banner.fromJson(x))), + details: List.from(json["details"].map((x) => Banner.fromJson(x))), + ); Map toJson() => { - "main": List.from(main.map((x) => x.toJson())), - "banners": List.from(banners.map((x) => x.toJson())), - "details": List.from(details.map((x) => x.toJson())), - }; + "main": List.from(main.map((x) => x.toJson())), + "banners": List.from(banners.map((x) => x.toJson())), + "details": List.from(details.map((x) => x.toJson())), + }; } class Banner { @@ -196,16 +196,16 @@ class Banner { String toRawJson() => json.encode(toJson()); factory Banner.fromJson(Map json) => Banner( - id: json["id"], - url: json["url"], - sequence: json["sequence"], - ); + id: json["id"], + url: json["url"], + sequence: json["sequence"], + ); Map toJson() => { - "id": id, - "url": url, - "sequence": sequence, - }; + "id": id, + "url": url, + "sequence": sequence, + }; } class OfferLine { @@ -214,10 +214,10 @@ class OfferLine { String procedureCode; String name; String nameAr; - int basePrice; - int discountedPrice; + double basePrice; + double discountedPrice; String discountType; - int discountValue; + double discountValue; int quantity; OfferLine({ @@ -238,28 +238,28 @@ class OfferLine { String toRawJson() => json.encode(toJson()); factory OfferLine.fromJson(Map 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"], - ); + 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 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, - }; -} \ No newline at end of file + "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, + }; +} diff --git a/lib/features/offers_and_discounts/models/offers_and_discounts_history_purchased_model.dart b/lib/features/offers_and_discounts/models/offers_and_discounts_history_purchased_model.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/features/offers_and_discounts/offers_and_discounts_repo.dart b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart index 953ce194..e175cd23 100644 --- a/lib/features/offers_and_discounts/offers_and_discounts_repo.dart +++ b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart @@ -68,10 +68,7 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo { @override Future>> getOfferDetails(int offerId) async { - Map headers = { - 'Content-Type': 'application/json', - 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey, - }; + Map headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey}; try { GenericApiModel? apiResponse; @@ -119,7 +116,7 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_OFFERS_AND_DISCOUNTS_HISTORY, // TODO: Replace with actual API endpoint + ApiConsts.pomsGetOffersAndDiscountPurchasedHistory, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart index c3f77056..d0166a9e 100644 --- a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart +++ b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart @@ -1,6 +1,6 @@ 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/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/services/error_handler_service.dart'; @@ -8,7 +8,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { bool isOffersLoading = false; bool isHistoryLoading = false; bool isOfferDetailsLoading = false; - List selectedFacilities = ['All Offers']; // Default to 'All Offers' + List selectedCategories = []; // Empty means 'All Offers' String searchQuery = ''; OffersAndDiscountsRepo offersAndDiscountsRepo; @@ -17,14 +17,13 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { List offersList = []; List historyList = []; OffersAndDiscountsDetailedResponseModel? offerDetails; + OffersAndDiscountsDetailedResponseModel? selectedOffer; Meta? meta; OffersAndDiscountsViewModel({ required this.offersAndDiscountsRepo, required this.errorHandlerService, - }) { - - } + }) {} // For loading state compatibility with existing code bool get isRegionListLoading => isOffersLoading; @@ -46,8 +45,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { notifyListeners(); } - setSelectedFacility(List facilities) { - selectedFacilities = facilities; + setSelectedCategories(List categoryIds) { + selectedCategories = categoryIds; notifyListeners(); } @@ -56,6 +55,25 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { notifyListeners(); } + /// Get unique categories from the offers list + List getUniqueCategories() { + if (offersList.isEmpty) return []; + + final Map 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 getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async { isOffersLoading = true; notifyListeners(); @@ -148,12 +166,10 @@ class OffersAndDiscountsViewModel extends ChangeNotifier { List get filteredOffers { List filtered = offersList; - // Filter by facility types (multi-select support) - if (!selectedFacilities.contains('All Offers')) { + // Filter by categories (if any categories are selected) + if (selectedCategories.isNotEmpty) { filtered = filtered.where((offer) { - // 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 + return selectedCategories.contains(offer.category.id); }).toList(); } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 8d29d818..86d8e65e 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1940,5 +1940,6 @@ abstract class LocaleKeys { static const refund = 'refund'; static const parkingQR = 'parkingQR'; static const parkingQRDesc = 'parkingQRDesc'; + static const paymentSuccessful = 'paymentSuccessful'; } diff --git a/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart b/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart index 4d377462..6be433a7 100644 --- a/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart +++ b/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.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/utils/size_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/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'; class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget { - final List? images; + final int offerId; final int initialIndex; const OfferAndDiscountsFullScreenSwiperPage({ super.key, - this.images, + required this.offerId, this.initialIndex = 0, }); @@ -26,21 +32,48 @@ class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget { class _OfferAndDiscountsFullScreenSwiperPageState extends State { late PageController _pageController; late int _currentPage; - late List _promoImages; + late OffersAndDiscountsViewModel viewModel; + late AppState appState; + List _promoImages = []; // Changed from late to regular variable @override void 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; _pageController = PageController(initialPage: widget.initialIndex); + + // Fetch offer details + scheduleMicrotask(() { + viewModel = Provider.of(context, listen: false); + viewModel.getOfferDetails(widget.offerId); + }); + } + + // Collect all images from the offer details + List _getAllImageUrls(OffersAndDiscountsDetailedResponseModel offer) { + List allImages = []; + + // Add main images + if (offer.images.main.isNotEmpty) { + allImages.addAll(offer.images.main.map((banner) => banner.url)); + } + + // Add banner images + if (offer.images.banners.isNotEmpty) { + allImages.addAll(offer.images.banners.map((banner) => banner.url)); + } + + // Add detail images + if (offer.images.details.isNotEmpty) { + allImages.addAll(offer.images.details.map((banner) => banner.url)); + } + + // If no images from the images object, use the main imageUrl + if (allImages.isEmpty && offer.imageUrl.isNotEmpty) { + allImages.add(offer.imageUrl); + } + + return allImages; } @override @@ -51,66 +84,178 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State(); + return Scaffold( backgroundColor: const Color(0xFFADA6D0), - body: SafeArea( - child: Stack( - children: [ - // Main content area - Column( - children: [ - SizedBox(height: 26.h), - - // Page indicators - _buildPageIndicators(), - - SizedBox(height: 70.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: getIt.get().isArabic(), - child: ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: Utils.buildImgWithAssets( - icon: _promoImages[index], - fit: BoxFit.contain, - ), - ), + body: Consumer( + builder: (context, vm, child) { + // Show loading state + if (vm.isOfferDetailsLoading) { + return SafeArea( + child: Stack( + children: [ + Center(child: Utils.getLoadingWidget()), + // Close button while loading + Positioned( + top: 26.h + 14.h, + right: 24.w, + child: _buildCloseButton(), + ), + ], + ), + ); + } + + final offer = vm.offerDetails; + if (offer == null) { + return SafeArea( + child: Stack( + children: [ + Center( + child: Text( + 'No offer details available', + style: TextStyle( + fontSize: 16.f, + color: AppColors.whiteColor, + ), + ), + ), + 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), - // Bottom action buttons - _buildBottomActions(), + SizedBox(height: 70.h), - 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 createState() => _OffersAndDiscountPaymentPageState(); +} + +class _OffersAndDiscountPaymentPageState extends State { + 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(); + myAppointmentsViewModel = Provider.of(context, listen: false); + offersAndDiscountsViewModel = Provider.of(context, listen: false); + payfortViewModel = Provider.of(context, listen: false); + paytabsViewModel = Provider.of(context, listen: false); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(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(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(); + }, + ); + } + }); + } +} diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart index 76e24eea..3b5e0aef 100644 --- a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart +++ b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.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_state.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/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart'; import 'package:hmg_patient_app_new/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 OffersAndDiscountsDetailedPage extends StatefulWidget { final int offerId; - const OffersAndDiscountsDetailedPage({ - super.key, - required this.offerId, - }); + const OffersAndDiscountsDetailedPage({super.key, required this.offerId}); @override State createState() => _OffersAndDiscountsDetailedPageState(); @@ -99,6 +99,7 @@ class _OffersAndDiscountsDetailedPageState extends State { 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 Widget build(BuildContext context) { @@ -68,6 +150,7 @@ class _OffersAndDiscountsPageState extends State { child: SingleChildScrollView( child: Consumer(builder: (context, offersAndDiscountVM, child) { return Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Search Input @@ -92,10 +175,13 @@ class _OffersAndDiscountsPageState extends State { ), SizedBox(height: 16.h), OffersAndDiscountTypeSelectionWidget( - selectedOffers: offersAndDiscountVM.selectedFacilities, - onOfferClicked: (selectedValues) { - offersAndDiscountVM.setSelectedFacility(selectedValues); + selectedCategories: offersAndDiscountVM.selectedCategories, + onCategoryClicked: (selectedCategoryIds) { + offersAndDiscountVM.setSelectedCategories(selectedCategoryIds); }, + categories: offersAndDiscountVM.getUniqueCategories(), + appState: appState, + isLoading: offersAndDiscountVM.isOffersLoading, ), SizedBox(height: 16.h), @@ -103,7 +189,7 @@ class _OffersAndDiscountsPageState extends State { // Offers Grid offersAndDiscountVM.isOffersLoading ? GridView.builder( - padding: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.symmetric(horizontal: 0.w), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( @@ -112,17 +198,10 @@ class _OffersAndDiscountsPageState extends State { mainAxisSpacing: 16.h, childAspectRatio: 0.55, // Adjust this to control card height ), - itemCount: 6, - // Show 6 loading placeholders + itemCount: 4, + // Show 4 loading placeholders itemBuilder: (context, index) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - child: Center(child: Utils.getLoadingWidget()), - ); + return _buildShimmerCard(); }, ) : offersAndDiscountVM.filteredOffers.isNotEmpty @@ -193,9 +272,7 @@ class _OffersAndDiscountsPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - appState.getLanguageID() == 1 - ? offersAndDiscountVM.filteredOffers[index].nameAr - : offersAndDiscountVM.filteredOffers[index].name, + appState.getLanguageID() == 1 ? offersAndDiscountVM.filteredOffers[index].nameAr : offersAndDiscountVM.filteredOffers[index].name, style: TextStyle( fontSize: 14.f, fontWeight: FontWeight.w700, @@ -221,13 +298,24 @@ class _OffersAndDiscountsPageState extends State { text: 'Buy Now', icon: AppAssets.shoppingCart, iconColor: AppColors.whiteColor, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: OffersAndDiscountsDetailedPage( - offerId: offersAndDiscountVM.filteredOffers[index].id, - ), - ), + onPressed: () async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context)); + await offersAndDiscountVM.getOfferDetails( + 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, @@ -262,3 +350,11 @@ class _OffersAndDiscountsPageState extends State { ); } } + +// Navigator.of(context).push( +// CustomPageRoute( +// page: OffersAndDiscountsDetailedPage( +// offerId: offersAndDiscountVM.filteredOffers[index].id, +// ), +// ), +// ); diff --git a/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart b/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart index 11713003..c62a17f5 100644 --- a/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart +++ b/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart @@ -1,68 +1,97 @@ 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/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/theme/colors.dart'; +import 'package:shimmer/shimmer.dart'; class OffersAndDiscountTypeSelectionWidget extends StatelessWidget { - final List selectedOffers; - final Function(List) onOfferClicked; + final List selectedCategories; + final Function(List) onCategoryClicked; + final List categories; + final AppState appState; + final bool isLoading; const OffersAndDiscountTypeSelectionWidget({ super.key, - required this.selectedOffers, - required this.onOfferClicked, + required this.selectedCategories, + required this.onCategoryClicked, + required this.categories, + required this.appState, + this.isLoading = false, }); @override Widget build(BuildContext context) { + final bool isArabic = appState.getLanguageID() == 1; + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ - _buildOfferTypeCard( - context: context, - title: "All Offers", - facilityType: 'All Offers', - isSelected: selectedOffers.contains('All Offers'), - ), - SizedBox(width: 12.w), - _buildOfferTypeCard( - context: context, - title: 'Female', - facilityType: 'Female', - isSelected: selectedOffers.contains('Female'), - ), - SizedBox(width: 12.w), - _buildOfferTypeCard( - context: context, - title: 'OB-Gyne', - facilityType: 'OB-Gyne', - isSelected: selectedOffers.contains('OB-Gyne'), - ), - SizedBox(width: 12.w), - _buildOfferTypeCard( - context: context, - title: 'Dermatology', - facilityType: 'Dermatology', - isSelected: selectedOffers.contains('Dermatology'), - ), - SizedBox(width: 12.w), - _buildOfferTypeCard( - context: context, - title: 'Radiology', - facilityType: 'Radiology', - isSelected: selectedOffers.contains('Radiology'), - ), + // Show loading skeleton chips + if (isLoading) ...[ + _buildLoadingChip(width: 80.w), + SizedBox(width: 12.w), + _buildLoadingChip(width: 80.w), + SizedBox(width: 12.w), + _buildLoadingChip(width: 120.w), + SizedBox(width: 12.w), + _buildLoadingChip(width: 95.w), + SizedBox(width: 12.w), + _buildLoadingChip(width: 110.w), + ] + // Show actual category chips + else ...[ + // "All Offers" button + _buildOfferTypeCard( + context: context, + title: isArabic ? "جميع العروض" : "All Offers", + categoryId: null, // null represents "All Offers" + isSelected: selectedCategories.isEmpty, + ), + SizedBox(width: 12.w), + + // Dynamic category buttons + ...categories + .map((category) => Padding( + padding: EdgeInsets.only(right: 12.w), + child: _buildOfferTypeCard( + context: context, + title: isArabic ? category.nameAr : category.name, + categoryId: category.id, + isSelected: selectedCategories.contains(category.id), + ), + )) + .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({ required BuildContext context, required String title, - required String facilityType, + required int? categoryId, // null means "All Offers" required bool isSelected, }) { return AnimatedContainer( @@ -82,32 +111,29 @@ class OffersAndDiscountTypeSelectionWidget extends StatelessWidget { ), ), ).onPress(() { - List updatedSelection = List.from(selectedOffers); + List updatedSelection = List.from(selectedCategories); - if (facilityType == 'All Offers') { - if (updatedSelection.contains('All Offers')) { + if (categoryId == null) { + // "All Offers" clicked + if (updatedSelection.isEmpty) { + // Already showing all, do nothing or toggle to empty updatedSelection.clear(); } else { + // Clear all selections to show all offers updatedSelection.clear(); - updatedSelection.add('All Offers'); } } else { - // Remove "All Offers" if any specific type is selected - updatedSelection.remove('All Offers'); - - // Toggle the clicked facility type - if (updatedSelection.contains(facilityType)) { - updatedSelection.remove(facilityType); - // If no selection left, default to "All Offers" - if (updatedSelection.isEmpty) { - updatedSelection.add('All Offers'); - } + // Specific category clicked + // Toggle the clicked category + if (updatedSelection.contains(categoryId)) { + updatedSelection.remove(categoryId); + // If no selection left, it means show all offers (empty list) } else { - updatedSelection.add(facilityType); + updatedSelection.add(categoryId); } } - onOfferClicked(updatedSelection); + onCategoryClicked(updatedSelection); }); } } diff --git a/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart b/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart index 408831be..37492185 100644 --- a/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart +++ b/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart @@ -1,8 +1,15 @@ import 'dart:async'; 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/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/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 '../../../core/utils/utils.dart'; @@ -17,21 +24,17 @@ class OffersAndDiscountsCarousel extends StatefulWidget { class _OffersAndDiscountsCarouselState extends State { final ScrollController _scrollController = ScrollController(); Timer? _autoScrollTimer; - - // List of offer images - you can add more images here - final List 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', - ]; + late AppState appState; @override void initState() { super.initState(); + appState = getIt.get(); + // Initialize the view model and load offers data + WidgetsBinding.instance.addPostFrameCallback((_) { + final viewModel = Provider.of(context, listen: false); + viewModel.initOffersAndDiscounts(); + }); // _startAutoScroll(); } @@ -65,23 +68,77 @@ class _OffersAndDiscountsCarouselState extends State @override Widget build(BuildContext context) { + return Consumer( + 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( height: 79.h, child: ListView.separated( - controller: _scrollController, scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: 0.w), - itemCount: offerImages.length * 100, + itemCount: 5, // Show 5 skeleton items itemBuilder: (context, index) { - final imageIndex = index % offerImages.length; - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const OfferAndDiscountsFullScreenSwiperPage()), - ); - }, - child: _buildOfferItem(offerImages[imageIndex]), + return Shimmer.fromColors( + baseColor: AppColors.greyColor.withValues(alpha: 0.3), + highlightColor: AppColors.whiteColor.withValues(alpha: 0.5), + child: Container( + width: 79.w, + height: 79.h, + decoration: ShapeDecoration( + color: AppColors.greyColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(13.r), + smoothness: 0.6, + ), + ), + ), ); }, separatorBuilder: (context, index) => SizedBox(width: 12.w), @@ -89,7 +146,7 @@ class _OffersAndDiscountsCarouselState extends State ); } - Widget _buildOfferItem(String imagePath) { + Widget _buildOfferItem(String imageUrl) { return Container( width: 79.w, height: 79.h, @@ -114,10 +171,30 @@ class _OffersAndDiscountsCarouselState extends State padding: const EdgeInsets.all(2.0), child: ClipRRect( borderRadius: BorderRadius.circular(11.r), - child: Utils.buildImgWithAssets( - icon: imagePath, - fit: BoxFit.cover, - ), + child: imageUrl.isNotEmpty + ? Utils.buildImgWithNetwork( + 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, + ), + ), ), ), ),