diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index 4f0d1b4..7fb07ac 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -7,6 +7,7 @@ import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; class OffersAndDiscountsApiClient { static final OffersAndDiscountsApiClient _instance = OffersAndDiscountsApiClient._internal(); @@ -75,10 +76,19 @@ class OffersAndDiscountsApiClient { String url = ApiConsts.walaPlusCategories; String? pUserName = AppState().getUserName; + if (pUserName == null || pUserName.isEmpty) { + pUserName = "2537367522"; + } String? tokenID = AppState().postParamsObject?.tokenID; + if (tokenID == null || tokenID.isEmpty || tokenID == "adm!n") { + tokenID = "@dm!n"; + } String? logInTokenID = AppState().postParamsObject?.logInTokenID; + if (logInTokenID == null || logInTokenID.isEmpty) { + logInTokenID = "@dm!n"; + } Map postParams = { "VersionID": 20, @@ -93,9 +103,15 @@ class OffersAndDiscountsApiClient { (response) { List categories = []; if (response != null && response['WalaPlus_Categories'] != null) { - response['WalaPlus_Categories'].forEach((v) { - categories.add(WalaPlusCategoryModel.fromJson(v)); - }); + var cats = response['WalaPlus_Categories']; + if (cats is String) { + cats = json.decode(cats); + } + if (cats is List) { + cats.forEach((v) { + categories.add(WalaPlusCategoryModel.fromJson(v)); + }); + } } return categories; }, @@ -103,4 +119,118 @@ class OffersAndDiscountsApiClient { postParams, ); } + + Future getWalaPlusRetailers({ + String? categoryId, + int page = 1, + int perPage = 10, + String? geoLocation, + BuildContext? context, + }) async { + String url = ApiConsts.walaPlusRetailers; + + String? pUserName = AppState().getUserName; + if (pUserName == null || pUserName.isEmpty) { + pUserName = "2537367522"; + } + + String? tokenID = AppState().postParamsObject?.tokenID; + if (tokenID == null || tokenID.isEmpty) { + tokenID = "adm!n"; + } + + String? logInTokenID = AppState().postParamsObject?.logInTokenID; + if (logInTokenID == null || logInTokenID.isEmpty) { + logInTokenID = "@dm!n"; + } + + Map postParams = { + "VersionID": 20, + "Channel": 31, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "LogInTokenID": logInTokenID, + "TokenID": tokenID, + "P_USER_NAME": pUserName, + "WP_CategoryID": (categoryId == "all" || categoryId == null) ? "" : categoryId, + "WP_Page": page, + "WP_Per_page": perPage, + }; + + if (geoLocation != null && geoLocation.isNotEmpty) { + postParams["WP_Geo_Location"] = geoLocation; + } + + return await ApiClient().postJsonForObject( + (response) { + if (response != null && response['WalaPlus_Retailers'] != null) { + var val = response['WalaPlus_Retailers']; + if (val is String) { + val = json.decode(val); + } + if (val is Map) { + return WalaPlusRetailersResponse.fromJson(val); + } else if (val is Map) { + return WalaPlusRetailersResponse.fromJson(Map.from(val)); + } + } + return null; + }, + url, + postParams, + ); + } + + Future getWalaPlusRetailerById({ + required String retailerId, + BuildContext? context, + }) async { + String url = ApiConsts.walaPlusRetailersByID; + + String? pUserName = AppState().getUserName; + if (pUserName == null || pUserName.isEmpty) { + pUserName = "2537367522"; + } + + String? tokenID = AppState().postParamsObject?.tokenID; + if (tokenID == null || tokenID.isEmpty) { + tokenID = "adm!n"; + } + + String? logInTokenID = AppState().postParamsObject?.logInTokenID; + if (logInTokenID == null || logInTokenID.isEmpty) { + logInTokenID = "@dm!n"; + } + + Map postParams = { + "VersionID": 20, + "Channel": 31, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "LogInTokenID": logInTokenID, + "TokenID": tokenID, + "RetailerID": retailerId, + "P_USER_NAME": pUserName, + }; + + return await ApiClient().postJsonForObject( + (response) { + if (response != null && response['WalaPlus_RetailersByID'] != null) { + var val = response['WalaPlus_RetailersByID']; + if (val is String) { + val = json.decode(val); + } + if (val is List && val.isNotEmpty) { + var item = val.first; + if (item is Map) { + return WalaPlusRetailerModel.fromJson(item); + } else if (item is Map) { + return WalaPlusRetailerModel.fromJson(Map.from(item)); + } + } + } + return null; + }, + url, + postParams, + ); + } } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 65ff8a3..74d1572 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -41,6 +41,8 @@ class ApiConsts { // WalaPlus Endpoints static String walaPlusCategories = erpRest + "WalaPlus_Categories"; + static String walaPlusRetailers = erpRest + "WalaPlus_Retailers"; + static String walaPlusRetailersByID = erpRest + "WalaPlus_RetailersByID"; static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect"; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 727cf5d..a9d868f 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -15,6 +15,7 @@ import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_ovr_home.dart'; import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_request_detailed.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen_for_dependet.dart'; +import 'package:mohem_flutter_app/ui/landing/wala_plus_retailer_details_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart'; import 'package:mohem_flutter_app/ui/landing/itg/survey_screen.dart'; @@ -99,6 +100,7 @@ class AppRoutes { static const String loginVerification = "/loginVerification"; static const String dashboard = "/dashboard"; static const String dashboardScreenForDependet = "/dashboardScreenForDependet"; + static const String walaPlusRetailerDetails = "/walaPlusRetailerDetails"; static const String todayAttendance = "/todayAttendance"; static const String subMenuScreen = "/submenuScreen"; static const String addEitScreen = "/addeitScreen"; @@ -234,6 +236,7 @@ class AppRoutes { verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(), dashboard: (BuildContext context) => DashboardScreen(), dashboardScreenForDependet: (BuildContext context) => const DashboardScreenForDependet(), + walaPlusRetailerDetails: (BuildContext context) => const WalaPlusRetailerDetailsScreen(), survey: (BuildContext context) => SurveyScreen(), advertisement: (BuildContext context) => ITGAdsScreen(), diff --git a/lib/dialogs/otp_dialog.dart b/lib/dialogs/otp_dialog.dart index e819b6f..b80b5e6 100644 --- a/lib/dialogs/otp_dialog.dart +++ b/lib/dialogs/otp_dialog.dart @@ -56,10 +56,12 @@ class OtpDialog { String? _code; dynamic setState; bool stopTimer = false; + bool isCodeExpired = false; // static String signature; void displayDialog(BuildContext context) async { + otpFieldClear.value = ""; showDialog( context: context, barrierColor: Colors.black.withOpacity(0.63), @@ -96,7 +98,20 @@ class OtpDialog { LocaleKeys.otp.tr().toText14(), LocaleKeys.verification.tr().toText24(isBold: true), 6.height, - (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + ' xxxxxxxx' + mobileNo.toString().substring(mobileNo.toString().length - 3)).toText16(), + Builder( + builder: (context) { + String maskedNumber = ""; + if (mobileNo != null) { + String numStr = mobileNo.toString(); + if (numStr.length >= 3) { + maskedNumber = ' xxxxxxxx' + numStr.substring(numStr.length - 3); + } else { + maskedNumber = ' ' + numStr; + } + } + return (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + maskedNumber).toText16(); + }, + ), 18.height, ValueListenableBuilder( builder: (BuildContext context, String value, Widget? child) { @@ -128,7 +143,7 @@ class OtpDialog { ), ), 10.height, - stopTimer + isCodeExpired ? Row( children: [ Expanded( @@ -164,18 +179,17 @@ class OtpDialog { ), 18.height, DefaultButton( - stopTimer ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), + isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), () async { - if (stopTimer) { + if (isCodeExpired) { hideSMSBox(context); onResendCode(); } else { stopTimer = true; - // onFailure(); hideSMSBox(context); } }, - colors: stopTimer + colors: isCodeExpired ? null : [ MyColors.pinkDarkColor, @@ -259,6 +273,7 @@ class OtpDialog { if (stopTimer) return; try { setState(() { + isCodeExpired = true; stopTimer = true; }); } catch (_) {} @@ -272,8 +287,8 @@ class OtpDialog { } void _onOtpCallBack(String otpCode, bool? isAutofill) { + if (isCodeExpired) return; if (otpCode.length == 4) { - stopTimer = true; otpFieldClear.value = otpCode; onSuccess(otpCode, _pinPutController); } diff --git a/lib/models/get_mobile_login_info_list_model.dart b/lib/models/get_mobile_login_info_list_model.dart index a2f7799..d544ef5 100644 --- a/lib/models/get_mobile_login_info_list_model.dart +++ b/lib/models/get_mobile_login_info_list_model.dart @@ -1,6 +1,6 @@ class GetMobileLoginInfoListModel { int? iD; - int? employeeID; + String? employeeID; int? channelID; int? companyID; String? deviceType; diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart new file mode 100644 index 0000000..39137fc --- /dev/null +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -0,0 +1,305 @@ +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; + +class WalaPlusRetailersResponse { + int? page; + int? pagesCount; + int? perPage; + List? retailers; + + WalaPlusRetailersResponse({ + this.page, + this.pagesCount, + this.perPage, + this.retailers, + }); + + factory WalaPlusRetailersResponse.fromJson(Map json) { + List retailersList = []; + if (json['retailers'] != null && json['retailers'] is List) { + json['retailers'].forEach((v) { + retailersList.add(WalaPlusRetailerModel.fromJson(v)); + }); + } + + return WalaPlusRetailersResponse( + page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? ''), + pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? ''), + perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? ''), + retailers: retailersList, + ); + } + + Map toJson() { + Map data = {}; + data['page'] = page; + data['pages_count'] = pagesCount; + data['per_page'] = perPage; + if (retailers != null) { + data['retailers'] = retailers!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class WalaPlusRetailerModel { + String? id; + String? name; + String? offerTitle; + String? offerType; + String? description; + String? info; + String? image; + String? imageThumb; + String? logo; + String? logoThumb; + String? website; + String? type; + dynamic usage; + dynamic weight; + dynamic geoId; + String? pin; + String? barcodeStandard; + String? arHowToUse; + String? enHowToUse; + String? howToUse; + WalaPlusRetailerContact? contact; + dynamic offerWeight; + List? locations; + List? categories; + + WalaPlusRetailerModel({ + this.id, + this.name, + this.offerTitle, + this.offerType, + this.description, + this.info, + this.image, + this.imageThumb, + this.logo, + this.logoThumb, + this.website, + this.type, + this.usage, + this.weight, + this.geoId, + this.pin, + this.barcodeStandard, + this.arHowToUse, + this.enHowToUse, + this.howToUse, + this.contact, + this.offerWeight, + this.locations, + this.categories, + }); + + factory WalaPlusRetailerModel.fromJson(Map json) { + List locationsList = []; + if (json['locations'] != null && json['locations'] is List) { + json['locations'].forEach((v) { + locationsList.add(WalaPlusRetailerLocation.fromJson(v)); + }); + } + + List categoriesList = []; + if (json['categories'] != null && json['categories'] is List) { + json['categories'].forEach((v) { + categoriesList.add(WalaPlusCategoryModel.fromJson(v)); + }); + } + + WalaPlusRetailerContact? contactObj; + if (json['contact'] != null) { + if (json['contact'] is Map) { + contactObj = WalaPlusRetailerContact.fromJson(Map.from(json['contact'])); + } else if (json['contact'] is String && json['contact'].toString().trim().isNotEmpty) { + contactObj = WalaPlusRetailerContact(website: json['contact'].toString()); + } + } + + return WalaPlusRetailerModel( + id: json['id']?.toString(), + name: json['name']?.toString(), + offerTitle: json['offer_title']?.toString(), + offerType: json['offer_type']?.toString(), + description: json['description']?.toString(), + info: json['info']?.toString(), + image: json['image']?.toString(), + imageThumb: json['image_thumb']?.toString(), + logo: json['logo']?.toString(), + logoThumb: json['logo_thumb']?.toString(), + website: json['website']?.toString(), + type: json['type']?.toString(), + usage: json['usage'], + weight: json['weight'], + geoId: json['geo_id'], + pin: json['pin']?.toString(), + barcodeStandard: json['barcode_standard']?.toString(), + arHowToUse: json['ar_how_to_use']?.toString(), + enHowToUse: json['en_how_to_use']?.toString(), + howToUse: json['how_to_use']?.toString(), + contact: contactObj, + offerWeight: json['offer_weight'], + locations: locationsList, + categories: categoriesList, + ); + } + + Map toJson() { + Map data = {}; + data['id'] = id; + data['name'] = name; + data['offer_title'] = offerTitle; + data['offer_type'] = offerType; + data['description'] = description; + data['info'] = info; + data['image'] = image; + data['image_thumb'] = imageThumb; + data['logo'] = logo; + data['logo_thumb'] = logoThumb; + data['website'] = website; + data['type'] = type; + data['usage'] = usage; + data['weight'] = weight; + data['geo_id'] = geoId; + data['pin'] = pin; + data['barcode_standard'] = barcodeStandard; + data['ar_how_to_use'] = arHowToUse; + data['en_how_to_use'] = enHowToUse; + data['how_to_use'] = howToUse; + data['contact'] = contact?.toJson(); + data['offer_weight'] = offerWeight; + if (locations != null) { + data['locations'] = locations!.map((v) => v.toJson()).toList(); + } + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } + + String? get resolvedWebsite { + if (contact?.website != null && contact!.website!.trim().isNotEmpty) { + return contact!.website!.trim(); + } + if (website != null && website!.trim().isNotEmpty) { + return website!.trim(); + } + return null; + } + + String get displayCity { + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if (loc.city != null && loc.city!.trim().isNotEmpty) { + return loc.city!.trim(); + } + } + } + if (type?.toLowerCase() == 'online') { + return 'Online'; + } + return ''; + } + + String get displayUsage { + if (usage != null && usage.toString().isNotEmpty) { + return 'Usage : $usage'; + } + if (type != null && type!.isNotEmpty) { + if (type!.toLowerCase() == 'online') return 'Online'; + if (type!.toLowerCase() == 'store') return 'In Store'; + return type!; + } + return ''; + } +} + +class WalaPlusRetailerContact { + List? emails; + List? phones; + String? website; + + WalaPlusRetailerContact({this.emails, this.phones, this.website}); + + factory WalaPlusRetailerContact.fromJson(Map json) { + List emailsList = []; + if (json['emails'] != null && json['emails'] is List) { + json['emails'].forEach((v) => emailsList.add(v.toString())); + } + List phonesList = []; + if (json['phones'] != null && json['phones'] is List) { + json['phones'].forEach((v) => phonesList.add(v.toString())); + } + return WalaPlusRetailerContact( + emails: emailsList, + phones: phonesList, + website: json['website']?.toString(), + ); + } + + Map toJson() { + return { + 'emails': emails, + 'phones': phones, + 'website': website, + }; + } +} + +class WalaPlusRetailerLocation { + String? address; + String? city; + String? cityNameAr; + String? cityNameEn; + String? contact; + String? countryNameAr; + String? countryNameEn; + double? latitude; + double? longitude; + dynamic placeId; + + WalaPlusRetailerLocation({ + this.address, + this.city, + this.cityNameAr, + this.cityNameEn, + this.contact, + this.countryNameAr, + this.countryNameEn, + this.latitude, + this.longitude, + this.placeId, + }); + + factory WalaPlusRetailerLocation.fromJson(Map json) { + return WalaPlusRetailerLocation( + address: json['address']?.toString(), + city: json['city']?.toString(), + cityNameAr: json['city_name_ar']?.toString(), + cityNameEn: json['city_name_en']?.toString(), + contact: json['contact']?.toString(), + countryNameAr: json['country_name_ar']?.toString(), + countryNameEn: json['country_name_en']?.toString(), + latitude: json['latitude'] is num ? (json['latitude'] as num).toDouble() : double.tryParse(json['latitude']?.toString() ?? ''), + longitude: json['longitude'] is num ? (json['longitude'] as num).toDouble() : double.tryParse(json['longitude']?.toString() ?? ''), + placeId: json['place_id'], + ); + } + + Map toJson() { + Map data = {}; + data['address'] = address; + data['city'] = city; + data['city_name_ar'] = cityNameAr; + data['city_name_en'] = cityNameEn; + data['contact'] = contact; + data['country_name_ar'] = countryNameAr; + data['country_name_en'] = countryNameEn; + data['latitude'] = latitude; + data['longitude'] = longitude; + data['place_id'] = placeId; + return data; + } +} diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index 4f4c455..0d7635a 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; @@ -11,10 +10,10 @@ import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; -import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; -import 'package:mohem_flutter_app/widgets/button/default_button.dart'; -import 'package:mohem_flutter_app/widgets/dynamic_forms/dynamic_textfield_widget.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:url_launcher/url_launcher.dart'; class DashboardScreenForDependet extends StatefulWidget { const DashboardScreenForDependet({Key? key}) : super(key: key); @@ -24,25 +23,54 @@ class DashboardScreenForDependet extends StatefulWidget { } class _DashboardScreenForDependetState extends State { - List offersList = []; - List _filteredOffersList = []; + List retailersList = []; + List _filteredRetailersList = []; List categoriesList = []; - String currentCategoryId = "0"; - bool isLoading = false; - bool isCategoriesLoading = false; + String currentCategoryId = "all"; + bool isNearestOffers = true; + bool isLoading = true; + bool isLoadingMore = false; + bool isCategoriesLoading = true; + bool hasMore = true; + bool _isFetchingPage = false; + int currentPage = 1; + int totalPages = 1; + final int pageSize = 10; String searchQuery = ""; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { - fetchCategories(); - fetchOffers(); + await fetchCategories(); } }); } + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + double maxScroll = _scrollController.position.maxScrollExtent; + double currentScroll = _scrollController.position.pixels; + // Trigger only when user scrolls to within 35px of the very bottom + if (currentScroll >= maxScroll - 35) { + if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { + fetchNextPage(); + } + } + } + Future fetchCategories() async { setState(() { isCategoriesLoading = true; @@ -50,43 +78,70 @@ class _DashboardScreenForDependetState extends State try { List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); - if (list.isNotEmpty) { - bool isAr = mounted ? AppState().isArabic(context) : false; - categoriesList = [ - WalaPlusCategoryModel( - id: "0", - name: isAr ? "الجميع" : "All", - image: null, - parentId: null, - ), - ...list, - ]; + bool isAr = mounted ? AppState().isArabic(context) : false; + WalaPlusCategoryModel allCategory = WalaPlusCategoryModel( + id: "all", + name: isAr ? "الكل" : "All", + ); + + if (mounted) { + setState(() { + categoriesList = [allCategory, ...list]; + currentCategoryId = "all"; + }); } } catch (e) { print("Error fetching WalaPlus categories: $e"); + if (mounted && categoriesList.isEmpty) { + bool isAr = AppState().isArabic(context); + setState(() { + categoriesList = [WalaPlusCategoryModel(id: "all", name: isAr ? "الكل" : "All")]; + currentCategoryId = "all"; + }); + } } finally { if (mounted) { setState(() { isCategoriesLoading = false; }); + // Default call without category filter ("all" -> passes "") + fetchOffers(categoryId: "all"); } } } - /// Offers API placeholder - will be connected when the endpoint is provided - Future fetchOffers() async { + Future fetchOffers({String? categoryId}) async { + String targetCategoryId = (categoryId != null && categoryId.isNotEmpty) + ? categoryId + : currentCategoryId; + setState(() { isLoading = true; + currentPage = 1; + hasMore = true; }); try { - // TODO: Call Dependent Offers API here once provided - // Example: - // offersList = await OffersApiClient().getDependentOffers(); - await Future.delayed(const Duration(milliseconds: 300)); - _runFilter(searchQuery); + String catParam = (targetCategoryId == "all") ? "" : targetCategoryId; + print("Fetching WalaPlus retailers: category='$catParam', page=1, per_page=$pageSize"); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( + categoryId: catParam, + page: 1, + perPage: pageSize, + geoLocation: isNearestOffers ? "24.9485,46.977" : null, + context: context, + ); + + retailersList = response?.retailers ?? []; + currentPage = 1; + totalPages = response?.pagesCount ?? 1; + hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false); + + _runFilter(_searchController.text); } catch (e) { - print("Error fetching dependent offers: $e"); + print("Error fetching WalaPlus retailers: $e"); + retailersList = []; + _runFilter(_searchController.text); } finally { if (mounted) { setState(() { @@ -96,31 +151,94 @@ class _DashboardScreenForDependetState extends State } } + Future fetchNextPage() async { + if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return; + + _isFetchingPage = true; + setState(() { + isLoadingMore = true; + }); + + try { + int nextPage = currentPage + 1; + String catParam = (currentCategoryId == "all") ? "" : currentCategoryId; + + print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam'"); + + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( + categoryId: catParam, + page: nextPage, + perPage: pageSize, + geoLocation: isNearestOffers ? "24.9485,46.977" : null, + context: context, + ); + + List newRetailers = response?.retailers ?? []; + if (newRetailers.isNotEmpty) { + currentPage = nextPage; + totalPages = response?.pagesCount ?? totalPages; + hasMore = currentPage < totalPages; + retailersList.addAll(newRetailers); + _runFilter(_searchController.text); + } else { + hasMore = false; + } + } catch (e) { + print("Error fetching next page of WalaPlus retailers: $e"); + } finally { + _isFetchingPage = false; + if (mounted) { + setState(() { + isLoadingMore = false; + }); + } + } + } + Future _onRefresh() async { - await Future.wait([ - fetchCategories(), - fetchOffers(), - ]); + await fetchCategories(); } void _runFilter(String query) { searchQuery = query; - List temp = offersList; - if (currentCategoryId != "0") { - temp = temp.where((offer) => offer.categoryId?.toString() == currentCategoryId).toList(); - } - if (query.isNotEmpty) { - bool isAr = AppState().isArabic(context); - temp = - temp.where((offer) { - String title = isAr ? (offer.titleAr ?? "") : (offer.titleEn ?? ""); - return title.toLowerCase().contains(query.toLowerCase()); - }).toList(); + List temp = retailersList; + if (query.trim().isNotEmpty) { + String q = query.trim().toLowerCase(); + temp = temp.where((item) { + String name = (item.name ?? "").toLowerCase(); + String title = (item.offerTitle ?? "").toLowerCase(); + String desc = (item.description ?? "").toLowerCase(); + String city = item.displayCity.toLowerCase(); + return name.contains(q) || title.contains(q) || desc.contains(q) || city.contains(q); + }).toList(); } - _filteredOffersList = temp; + _filteredRetailersList = temp; setState(() {}); } + String _getUserDisplayName() { + if (AppState().memberInformationList?.eMPLOYEENAME != null && + AppState().memberInformationList!.eMPLOYEENAME!.trim().isNotEmpty) { + return AppState().memberInformationList!.eMPLOYEENAME!.trim(); + } + bool isAr = AppState().isArabic(context); + String? dispName = isAr + ? AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEAr + : AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEEn; + if (dispName != null && dispName.trim().isNotEmpty) { + return dispName.trim(); + } + return "Test User"; + } + + void _openRetailer(WalaPlusRetailerModel item) { + Navigator.pushNamed( + context, + AppRoutes.walaPlusRetailerDetails, + arguments: item, + ); + } + void _performLogout() { Utils.confirmDialog( context, @@ -139,224 +257,591 @@ class _DashboardScreenForDependetState extends State bool isAr = AppState().isArabic(context); return Scaffold( - backgroundColor: MyColors.backgroundColor, + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, surfaceTintColor: Colors.transparent, automaticallyImplyLeading: false, - title: Row(children: [(isAr ? "عروض التابعين" : "Offers & Discounts").toText20(color: MyColors.darkTextColor, isBold: true)]), - actions: [ - // Logout button - Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration(color: MyColors.redColor.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: _performLogout, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [const Icon(Icons.logout_rounded, size: 18, color: MyColors.redColor), 6.width, (isAr ? "خروج" : "Logout").toText13(color: MyColors.redColor)], - ), - ), - ), - ), - ], + toolbarHeight: 0, ), - body: RefreshIndicator( - onRefresh: _onRefresh, - color: MyColors.textMixColor, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - padding: const EdgeInsets.all(21), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Search input - DynamicTextFieldWidget( - LocaleKeys.search.tr(), - LocaleKeys.searchItems.tr(), - isEnable: true, - suffixIconData: Icons.search, - isPopup: false, - lines: 1, - isInputTypeNum: false, - isReadOnly: false, - onChange: (String value) { - _runFilter(value); - }, - ), - 20.height, - - // Categories Section - if (isCategoriesLoading) - _buildCategoriesShimmer() - else if (categoriesList.isNotEmpty) ...[ - LocaleKeys.browseCategories.tr().toText17(isBold: true, color: MyColors.darkTextColor), - 12.height, - SizedBox( - height: 110.0, - child: ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: categoriesList.length, - separatorBuilder: (cxt, index) => 12.width, - itemBuilder: (BuildContext cxt, int index) { - WalaPlusCategoryModel item = categoriesList[index]; - bool isSelected = currentCategoryId == item.id; - String title = item.id == "0" ? (isAr ? "الجميع" : "All") : (item.name ?? ""); - - return AspectRatio( - aspectRatio: 1 / 1, - child: InkWell( - borderRadius: BorderRadius.circular(15), + body: SafeArea( + child: RefreshIndicator( + onRefresh: _onRefresh, + color: MyColors.textMixColor, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Header: Welcome back & User Name & Logout + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAr ? "مرحبا بعودتك" : "Welcome back", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF6B7280), + ), + ), + 4.height, + Text( + _getUserDisplayName(), + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: Color(0xFF111827), + letterSpacing: -0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + 10.width, + // Logout button + InkWell( + borderRadius: BorderRadius.circular(10), + onTap: _performLogout, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: MyColors.redColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.logout_rounded, size: 20, color: MyColors.redColor), + ), + ), + ], + ), + 20.height, + + // Search Input Box + _buildSearchBar(isAr), + 18.height, + + // Nearest Offers Checkbox + _buildNearestOffersRow(isAr), + 16.height, + + // Categories Section - Always preserved once loaded with "All" prepended + if (isCategoriesLoading && categoriesList.isEmpty) + _buildCategoriesShimmer() + else if (categoriesList.isNotEmpty) ...[ + SizedBox( + height: 42.0, + width: double.infinity, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: categoriesList.length, + separatorBuilder: (cxt, index) => 10.width, + itemBuilder: (BuildContext cxt, int index) { + WalaPlusCategoryModel item = categoriesList[index]; + bool isSelected = currentCategoryId == item.id; + String title = item.name ?? ""; + + return InkWell( + borderRadius: BorderRadius.circular(24), onTap: () { - setState(() { - currentCategoryId = item.id ?? "0"; - }); - _runFilter(searchQuery); + if (currentCategoryId != item.id) { + setState(() { + currentCategoryId = item.id ?? "all"; + }); + fetchOffers(categoryId: currentCategoryId); + } }, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildCategoryIcon(item), - if (isSelected) - const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0), - ], - ).expanded, - title.toText10( - isBold: isSelected, - color: isSelected ? MyColors.textMixColor : MyColors.darkTextColor, - maxlines: 2, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF212C36) : Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: isSelected ? const Color(0xFF212C36) : const Color(0xFFE8ECEF), + width: 1.2, ), - ], - ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), + ), + child: Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? Colors.white : const Color(0xFF2C3E50), + ), + ), + ), + ); + }, + ), + ), + 22.height, + ], + + // "Popular offers" Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + isAr ? "أشهر العروض" : "Popular offers", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFF111827), + ), + ), + InkWell( + onTap: () { + fetchOffers(categoryId: currentCategoryId); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), + child: Text( + isAr ? "عرض الكل" : "See all", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFFE53935), + ), ), - ); + ), + ), + ], + ), + 16.height, + + // Content Area - Shimmer while loading retailer data + if (isLoading) + _buildRetailersShimmer() + else if (_filteredRetailersList.isNotEmpty) ...[ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.68, + crossAxisSpacing: 14, + mainAxisSpacing: 14, + ), + itemCount: _filteredRetailersList.length, + itemBuilder: (context, index) { + return _buildOfferCard(_filteredRetailersList[index], isAr); }, ), - ), - 20.height, + if (isLoadingMore) ...[ + 16.height, + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: MyColors.textMixColor, + ), + ), + ), + ), + ], + ] else + const SizedBox.shrink(), ], + ), + ), + ), + ), + ); + } - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - (isAr ? "العروض المتاحة" : "Available Offers").toText18(isBold: true, color: MyColors.darkTextColor), - if (offersList.isNotEmpty) "${_filteredOffersList.length} ${isAr ? "عرض" : "offers"}".toText12(color: MyColors.grey70Color), - ], - ), - 16.height, - - // Content Area - if (isLoading) - const Center(child: Padding(padding: EdgeInsets.symmetric(vertical: 60), child: CircularProgressIndicator(color: MyColors.textMixColor))) - else if (_filteredOffersList.isEmpty) - _buildEmptyOffersWidget(isAr) - else - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 162 / 266, crossAxisSpacing: 12, mainAxisSpacing: 12), - itemCount: _filteredOffersList.length, - itemBuilder: (context, index) { - return _buildOfferCard(_filteredOffersList[index], isAr); - }, + Widget _buildSearchBar(bool isAr) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isAr ? "بحث" : "Search", + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF6B7280), + ), ), - ], + TextField( + controller: _searchController, + onChanged: _runFilter, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 3), + hintText: isAr ? "بحث" : "Search", + hintStyle: const TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + ), + border: InputBorder.none, + ), + style: const TextStyle( + color: Color(0xFF111827), + fontSize: 14, + ), + ), + ], + ), ), - ), + const Icon(Icons.search, color: Color(0xFF6B7280), size: 22), + ], ), ); } - Widget _buildEmptyOffersWidget(bool isAr) { - return Center( + Widget _buildNearestOffersRow(bool isAr) { + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + setState(() { + isNearestOffers = !isNearestOffers; + }); + fetchOffers(categoryId: currentCategoryId); + }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 60, horizontal: 24), - child: Column( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( - width: 80, - height: 80, - decoration: BoxDecoration(color: MyColors.textMixColor.withOpacity(0.1), shape: BoxShape.circle), - child: const Icon(Icons.local_offer_outlined, size: 40, color: MyColors.textMixColor), + width: 22, + height: 22, + decoration: BoxDecoration( + color: isNearestOffers ? const Color(0xFF00A651) : Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isNearestOffers ? const Color(0xFF00A651) : const Color(0xFFD1D5DB), + width: 1.5, + ), + ), + child: isNearestOffers ? const Icon(Icons.check, size: 16, color: Colors.white) : null, + ), + 10.width, + Text( + isAr ? "العروض الأقرب" : "Nearest offers", + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), ), - 16.height, - (isAr ? "لا توجد عروض متاحة حالياً" : "No Offers Available Currently").toText16(isBold: true, color: MyColors.darkTextColor), - 8.height, - (isAr ? "سيتم إتاحة العروض الخاصة بك قريباً" : "Exclusive offers for dependents will be available soon.").toText13(color: MyColors.grey70Color), - 24.height, - DefaultButton(isAr ? "تحديث" : "Refresh", fetchOffers, colors: const [MyColors.gradiantStartColor, MyColors.gradiantEndColor]), ], ), ), ); } - Widget _buildOfferCard(OffersListModel item, bool isAr) { - return Container( - padding: const EdgeInsets.all(10.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 26, offset: const Offset(0, -3))], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - AspectRatio( - aspectRatio: 118 / 127, - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: - (item.bannerImage != null && item.bannerImage!.isNotEmpty) - ? Image.network( - item.bannerImage!, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => Container(color: MyColors.greyF7Color, child: const Icon(Icons.broken_image, color: MyColors.grey70Color)), - ) - : Container(color: MyColors.greyF7Color, child: const Icon(Icons.image_outlined, color: MyColors.grey70Color)), + Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) { + String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty) + ? item.imageThumb + : item.image; + String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) + ? item.logoThumb + : item.logo; + + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _openRetailer(item), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 3), ), - ), - 8.height, - (isAr ? (item.titleAr ?? "") : (item.titleEn ?? "")).toText16(isBold: true, color: MyColors.darkTextColor, maxlines: 1), - if (item.discountDescription != null && item.discountDescription!.isNotEmpty) ...[4.height, item.discountDescription!.toText14(isBold: true, maxlines: 1)], - ], + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Top Image with Logo Badge overlay + SizedBox( + height: 110, + width: double.infinity, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Banner Image + Positioned.fill( + child: (bannerUrl != null && bannerUrl.isNotEmpty) + ? Image.network( + bannerUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.broken_image, color: Color(0xFF9CA3AF), size: 36), + ), + ) + : Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.image_outlined, color: Color(0xFF9CA3AF), size: 36), + ), + ), + + // Brand Logo Badge centered at top + if (logoUrl != null && logoUrl.isNotEmpty) + Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFE5E7EB), width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Image.network( + logoUrl, + height: 20, + width: 40, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox(width: 40, height: 20), + ), + ), + ), + ], + ), + ), + + // Card Body + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Retailer Name / Bundle Title + Text( + item.name ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Color(0xFF1E293B), + height: 1.2, + ), + ), + const SizedBox(height: 3), + + // Offer Title / Subtitle + Text( + item.offerTitle ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: Color(0xFF64748B), + ), + ), + const SizedBox(height: 6), + + // Badges Row 1: "Offer Valid" & "Usage : X" + Wrap( + spacing: 5, + runSpacing: 4, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + isAr ? "العرض ساري" : "Offer Valid", + style: const TextStyle( + color: Color(0xFF00A651), + fontSize: 9.5, + fontWeight: FontWeight.w600, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFFEF3C7), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + item.displayUsage, + style: const TextStyle( + color: Color(0xFFB45309), + fontSize: 9.5, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 5), + + // Location Badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + item.displayCity, + style: const TextStyle( + color: Color(0xFF4B5563), + fontSize: 9.5, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), ), ); } - Widget _buildCategoryIcon(WalaPlusCategoryModel item) { - if (item.id == "0" || item.image == null || item.image!.isEmpty) { - return const Icon(Icons.grid_view_rounded, size: 24, color: MyColors.textMixColor); - } - if (item.isSvg) { - return SvgPicture.network( - item.image!, - width: 25, - height: 25, - fit: BoxFit.contain, - placeholderBuilder: (_) => const SizedBox(width: 25, height: 25), - ); - } - return Image.network( - item.imageThumb ?? item.image!, - width: 25, - height: 25, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => const Icon(Icons.category_outlined, size: 24, color: MyColors.grey70Color), + Widget _buildRetailersShimmer() { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.68, + crossAxisSpacing: 14, + mainAxisSpacing: 14, + ), + itemCount: 4, + itemBuilder: (_, __) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Top image placeholder + Container( + height: 110, + width: double.infinity, + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 13, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 3), + Container( + height: 11, + width: 75, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Container( + height: 16, + width: 52, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + ), + const SizedBox(width: 5), + Container( + height: 16, + width: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + const SizedBox(height: 5), + Container( + height: 16, + width: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), ); } @@ -364,25 +849,31 @@ class _DashboardScreenForDependetState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container(width: 120, height: 16, color: Colors.white).toShimmer(), - 12.height, SizedBox( - height: 110.0, - child: ListView.separated( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - itemCount: 4, - separatorBuilder: (_, __) => 12.width, - itemBuilder: (_, __) => AspectRatio( - aspectRatio: 1 / 1, - child: Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15)), - ).toShimmer(), + height: 42.0, + child: Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 5, + separatorBuilder: (_, __) => 10.width, + itemBuilder: (_, index) { + return Container( + width: index == 0 ? 80 : (index % 2 == 0 ? 120 : 95), + height: 42, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + ), + ); + }, ), ), ), - 20.height, + 22.height, ], ); } diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/landing/wala_plus_retailer_details_screen.dart new file mode 100644 index 0000000..a4b441b --- /dev/null +++ b/lib/ui/landing/wala_plus_retailer_details_screen.dart @@ -0,0 +1,630 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class WalaPlusRetailerDetailsScreen extends StatefulWidget { + final WalaPlusRetailerModel? retailer; + final String? retailerId; + + const WalaPlusRetailerDetailsScreen({ + Key? key, + this.retailer, + this.retailerId, + }) : super(key: key); + + @override + State createState() => _WalaPlusRetailerDetailsScreenState(); +} + +class _WalaPlusRetailerDetailsScreenState extends State { + WalaPlusRetailerModel? _retailer; + bool _isLoading = false; + String? _effectiveRetailerId; + + @override + void initState() { + super.initState(); + _retailer = widget.retailer; + _effectiveRetailerId = widget.retailerId ?? widget.retailer?.id; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_retailer == null && _effectiveRetailerId == null) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is WalaPlusRetailerModel) { + _retailer = args; + _effectiveRetailerId = args.id; + } else if (args is String) { + _effectiveRetailerId = args; + } + } + + if (_retailer == null && _effectiveRetailerId != null && !_isLoading) { + _fetchRetailerDetails(); + } + } + + Future _fetchRetailerDetails() async { + if (_effectiveRetailerId == null) return; + setState(() { + _isLoading = true; + }); + + try { + WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient().getWalaPlusRetailerById( + retailerId: _effectiveRetailerId!, + context: context, + ); + if (result != null && mounted) { + setState(() { + _retailer = result; + }); + } + } catch (e) { + debugPrint("Error fetching WalaPlus retailer by ID: $e"); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _onUseOffer() async { + String? website = _retailer?.resolvedWebsite; + if (website != null && website.isNotEmpty) { + try { + Uri uri = Uri.parse(website); + if (!uri.hasScheme) { + uri = Uri.parse("https://$website"); + } + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } catch (e) { + debugPrint("Error launching website: $e"); + } + } + + // If promo pin exists, display a dialog with the promo code to copy + if (_retailer?.pin != null && _retailer!.pin!.isNotEmpty) { + _showPromoCodeDialog(_retailer!.pin!); + return; + } + + // Otherwise show terms/usage dialog + _showTermsDialog(); + } + + void _showPromoCodeDialog(String pin) { + bool isAr = AppState().isArabic(context); + showDialog( + context: context, + builder: (cxt) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: Text( + isAr ? "كود الخصم" : "Promo Code", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF20A4A8), width: 1.2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + pin, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 2, + color: Color(0xFF1E293B), + ), + ), + IconButton( + icon: const Icon(Icons.copy_rounded, color: Color(0xFF20A4A8)), + onPressed: () { + Clipboard.setData(ClipboardData(text: pin)); + Navigator.pop(cxt); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(isAr ? "تم نسخ الكود بنجاح" : "Promo code copied"), + duration: const Duration(seconds: 2), + ), + ); + }, + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(cxt), + child: Text(isAr ? "إغلاق" : "Close", style: const TextStyle(color: Color(0xFF20A4A8))), + ), + ], + ), + ); + } + + void _showTermsDialog() { + bool isAr = AppState().isArabic(context); + String content = _retailer?.howToUse ?? + _retailer?.arHowToUse ?? + _retailer?.enHowToUse ?? + _retailer?.usage?.toString() ?? + (isAr ? "تطبق الشروط والأحكام الخاصة بالعرض." : "Terms and conditions apply for this offer."); + + // Clean html tags if simple + content = content.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim(); + + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (cxt) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isAr ? "الشروط والأحكام" : "Terms and Conditions", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1E293B), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(cxt), + ), + ], + ), + const SizedBox(height: 12), + Text( + content, + style: const TextStyle(fontSize: 14, color: Color(0xFF4B5563), height: 1.5), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + String title = _retailer?.name ?? (isAr ? "تفاصيل العرض" : "Offer Details"); + + return Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBarWidget( + context, + title: title, + showHomeButton: true, + onHomeTapped: () { + if (ApiConsts.isDependetLogin) { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboardScreenForDependet)); + } else { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); + } + }, + ), + body: _isLoading && _retailer == null + ? _buildLoadingSkeleton() + : Column( + children: [ + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: const Color(0xFFF1F5F9), width: 1.2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Top Banner Image with rounded corners + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SizedBox( + height: 190, + width: double.infinity, + child: (_retailer?.image != null && _retailer!.image!.isNotEmpty) + ? Image.network( + _retailer!.image!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF)), + ), + ) + : Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF)), + ), + ), + ), + 14.height, + + // 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib) + _buildBrandStrip(isAr), + 18.height, + + // 3. Retailer Name (Centered, Bold) + Center( + child: Text( + _retailer?.name ?? "", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF1E293B), + ), + ), + ), + 18.height, + + // 4. 3 Info Badges Row (Store Type, Validity, City) + _buildInfoBadgesRow(isAr), + 22.height, + + // 5. Description Section + Text( + isAr ? "الوصف:" : "description:", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1E293B), + ), + ), + 8.height, + Text( + _getCleanDescription(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Color(0xFF4B5563), + height: 1.45, + ), + ), + 16.height, + + // 6. Terms and Conditions Link (Right aligned) + Align( + alignment: isAr ? Alignment.centerLeft : Alignment.centerRight, + child: InkWell( + onTap: _showTermsDialog, + child: Text( + isAr ? "الشروط والأحكام" : "terms and conditions", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF20A4A8), + decoration: TextDecoration.underline, + ), + ), + ), + ), + ], + ), + ), + ), + ), + + // 7. Bottom Action Button + Container( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 20), + color: Colors.white, + child: SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _onUseOffer, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF20A4A8), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + isAr ? "استخدام العرض" : "Use Offer", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildBrandStrip(bool isAr) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left: Human Resources branding + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + "الموارد البشرية", + style: TextStyle( + color: Color(0xFFED1C2B), + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + Text( + "Human Resources", + style: TextStyle( + color: Color(0xFFED1C2B), + fontSize: 8.5, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + + // Center: Retailer Logo + if (_retailer?.logo != null && _retailer!.logo!.isNotEmpty) + Image.network( + _retailer!.logo!, + height: 36, + width: 36, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36), + ) + else + const SizedBox(width: 36, height: 36), + + // Right: Dr Sulaiman Al Habib branding with red cross + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + "سليمان الحبيب", + style: TextStyle( + color: Color(0xFF4B5563), + fontSize: 9, + fontWeight: FontWeight.w700, + ), + ), + Text( + "DR SULAIMAN AL HABIB", + style: TextStyle( + color: Color(0xFF4B5563), + fontSize: 8, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(width: 5), + SvgPicture.asset( + "assets/icons/mazaya_brand.svg", + height: 22, + fit: BoxFit.contain, + ), + ], + ), + ], + ); + } + + Widget _buildInfoBadgesRow(bool isAr) { + String storeTypeText = (_retailer?.type?.toLowerCase() == 'online') + ? (isAr ? "عبر الإنترنت" : "online") + : (isAr ? "في المتجر" : "in store"); + + IconData storeIcon = (_retailer?.type?.toLowerCase() == 'online') + ? Icons.language_rounded + : Icons.storefront_rounded; + + String cityText = _retailer?.displayCity ?? ""; + if (cityText.isEmpty) { + cityText = isAr ? "الرياض" : "Riyadh"; + } + + String validityText = isAr ? "٣ أشهر" : "3 Month"; + + return Row( + children: [ + Expanded( + child: _buildInfoCard( + icon: storeIcon, + label: storeTypeText, + ), + ), + 12.width, + Expanded( + child: _buildInfoCard( + icon: Icons.check_circle_outline_rounded, + label: validityText, + ), + ), + 12.width, + Expanded( + child: _buildInfoCard( + icon: Icons.location_on_rounded, + label: cityText, + ), + ), + ], + ); + } + + Widget _buildInfoCard({required IconData icon, required String label}) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFFF4F6F9), + borderRadius: BorderRadius.circular(14), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: const Color(0xFF20A4A8), size: 26), + const SizedBox(height: 8), + Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF334155), + ), + ), + ], + ), + ); + } + + String _getCleanDescription() { + String desc = _retailer?.description ?? _retailer?.info ?? _retailer?.offerTitle ?? ""; + if (desc.isEmpty) { + return AppState().isArabic(context) ? "لا توجد تفاصيل إضافية متاحة." : "No additional details available."; + } + // Remove HTML tags if string contains them + return desc.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim(); + } + + Widget _buildLoadingSkeleton() { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 190, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + const SizedBox(height: 16), + Center( + child: Container( + height: 24, + width: 160, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + const SizedBox(height: 20), + Row( + children: List.generate( + 3, + (index) => Expanded( + child: Container( + height: 75, + margin: EdgeInsets.symmetric(horizontal: index == 1 ? 8 : 0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), + ), + ), + ), + ), + const SizedBox(height: 24), + Container( + height: 16, + width: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 10), + Container( + height: 14, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 6), + Container( + height: 14, + width: 220, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ), + ); + } +}