From fe36263ac08f157a130bfff7cd07237089a68a6c Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 22 Oct 2026 09:24:23 +0300 Subject: [PATCH] design changed --- assets/langs/ar-SA.json | 22 +- assets/langs/en-US.json | 22 +- lib/api/offers_and_discounts_api_client.dart | 27 +- .../wala_plus_category_model.dart | 6 +- .../wala_plus_retailer_model.dart | 163 ++- .../dashboard_screen_for_dependet.dart | 939 +++++++------ .../wala_plus_retailer_details_screen.dart | 1199 ++++++++--------- 7 files changed, 1332 insertions(+), 1046 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 2110db11..4861521b 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -830,5 +830,25 @@ "locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك", "locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك", "favoriteMerchant": "العروض المفضلة", - "favoriteOffers": "العروض المفضلة" + "favoriteOffers": "العروض المفضلة", + "goodToKnow": "معلومات مهمة", + "goodToKnowDesc": "احتفظ بعضوية مزايا في متناول يدك. تحقق من توفر التاجر والأنشطة المؤهلة والشروط الكاملة قبل الحجز.", + "termsAndConditions": "الشروط والأحكام", + "haveAQuestionAboutOffer": "هل لديك سؤال حول هذا العرض؟", + "getHelp": "الحصول على المساعدة", + "redeemOffer": "استرداد العرض", + "merchantContact": "بيانات التواصل مع التاجر", + "website": "الموقع الإلكتروني", + "phone": "الهاتف", + "viewAll": "عرض الكل", + "moreCitiesCount": "+ {} مدن أخرى", + "mazayaDeals": "عروض مزايا", + "exploreYourBenefits": "استكشف مزاياك", + "findAnEscapeOrAnExperience": "ابحث عن تجربة أو عطلة", + "allOffers": "جميع العروض", + "sampleAvailability": "حسب التوفر", + "offersCount": "{} عروض", + "mazayaExclusive": "حصري لمزايا", + "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", + "selectCity": "اختر المدينة" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index ffe888b6..47bfca8a 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -834,6 +834,26 @@ "favoriteRemovedSuccess": "Removed from favorites successfully", "favoriteError": "An error occurred while updating favorites", "favoriteMerchant": "Favorite Offers", - "favoriteOffers": "Favorite Offers" + "favoriteOffers": "Favorite Offers", + "goodToKnow": "Good to know", + "goodToKnowDesc": "Keep your Mazaya membership handy. Check the merchant's availability, eligible activities and full terms before you book.", + "termsAndConditions": "Terms & conditions", + "haveAQuestionAboutOffer": "Have a question about this offer?", + "getHelp": "Get help", + "redeemOffer": "Redeem offer", + "merchantContact": "Merchant contact", + "website": "Website", + "phone": "Phone", + "viewAll": "View all", + "moreCitiesCount": "+ {} more cities", + "mazayaDeals": "Mazaya Deals", + "exploreYourBenefits": "Explore your benefits", + "findAnEscapeOrAnExperience": "Find an escape or an experience", + "allOffers": "All offers", + "sampleAvailability": "sample availability", + "offersCount": "{} offers", + "mazayaExclusive": "MAZAYA EXCLUSIVE", + "youAreAllCaughtUp": "You're all caught up. More little extras soon.", + "selectCity": "Select City" } diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index 79ec90fe..1ad81445 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -16,6 +16,7 @@ import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; class OffersAndDiscountsApiClient { static final OffersAndDiscountsApiClient _instance = OffersAndDiscountsApiClient._internal(); + static List? cachedCategories; OffersAndDiscountsApiClient._internal(); @@ -204,7 +205,7 @@ class OffersAndDiscountsApiClient { } _cleanWalaPlusParams(postParams); - return await ApiClient().postJsonForObject( + List list = await ApiClient().postJsonForObject( (response) { List categories = []; if (response != null) { @@ -231,6 +232,10 @@ class OffersAndDiscountsApiClient { url, postParams, ); + if ((parentId == null || parentId.trim().isEmpty) && list.isNotEmpty) { + cachedCategories = list; + } + return list; } // Confirmed with backend team: WP_Offer_Type accepts "online" and "store". @@ -446,6 +451,16 @@ class OffersAndDiscountsApiClient { model?.website = web.trim(); } } + if ((model?.categories == null || model!.categories!.isEmpty) && (map.containsKey('categories') || map.containsKey('Categories') || map.containsKey('category') || map.containsKey('Category'))) { + dynamic catData = map['categories'] ?? map['Categories'] ?? map['category'] ?? map['Category']; + if (catData is List) { + model?.categories = catData.map((c) => c is Map ? WalaPlusCategoryModel.fromJson(Map.from(c)) : WalaPlusCategoryModel(name: c.toString())).toList(); + } else if (catData is Map) { + model?.categories = [WalaPlusCategoryModel.fromJson(Map.from(catData))]; + } else if (catData is String && catData.trim().isNotEmpty) { + model?.categories = [WalaPlusCategoryModel(name: catData.trim())]; + } + } } else if (response is Map && response.containsKey('name')) { model = WalaPlusRetailerModel.fromJson(response); } @@ -472,6 +487,16 @@ class OffersAndDiscountsApiClient { (response.containsKey('locations') || response.containsKey('Locations') || response.containsKey('location') || response.containsKey('Location'))) { model.locations = WalaPlusRetailerModel.parseLocationsList(response['locations'] ?? response['Locations'] ?? response['location'] ?? response['Location']); } + if ((model.categories == null || model.categories!.isEmpty) && (response.containsKey('categories') || response.containsKey('Categories') || response.containsKey('category') || response.containsKey('Category'))) { + dynamic catData = response['categories'] ?? response['Categories'] ?? response['category'] ?? response['Category']; + if (catData is List) { + model.categories = catData.map((c) => c is Map ? WalaPlusCategoryModel.fromJson(Map.from(c)) : WalaPlusCategoryModel(name: c.toString())).toList(); + } else if (catData is Map) { + model.categories = [WalaPlusCategoryModel.fromJson(Map.from(catData))]; + } else if (catData is String && catData.trim().isNotEmpty) { + model.categories = [WalaPlusCategoryModel(name: catData.trim())]; + } + } } debugPrint("getWalaPlusRetailerById: parsed retailer '${model.name}', isFavorite: ${model.isFavorite}, offers: ${model.offers?.length ?? 0}, locations: ${model.locations?.length ?? 0}"); return model; diff --git a/lib/models/offers_and_discounts/wala_plus_category_model.dart b/lib/models/offers_and_discounts/wala_plus_category_model.dart index c0995277..0bd04fe8 100644 --- a/lib/models/offers_and_discounts/wala_plus_category_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_category_model.dart @@ -28,11 +28,11 @@ class WalaPlusCategoryModel { WalaPlusCategoryModel.fromJson(Map json) { id = (json['id'] ?? json['Id'] ?? json['ID'])?.toString(); - name = (json['name'] ?? json['Name'])?.toString(); + name = (json['name'] ?? json['Name'] ?? json['category_name'] ?? json['categoryName'] ?? json['cat_name'] ?? json['CategoryName'])?.toString(); parentId = (json['parent_id'] ?? json['Parent_Id'] ?? json['parentId'])?.toString(); imageThumb = (json['image_thumb'] ?? json['Image_Thumb'] ?? json['imageThumb'])?.toString(); - countryNameAr = (json['country_name_ar'] ?? json['countryNameAr'] ?? json['category_name_ar'])?.toString(); - countryNameEn = (json['country_name_en'] ?? json['countryNameEn'] ?? json['category_name_en'])?.toString(); + countryNameAr = (json['country_name_ar'] ?? json['countryNameAr'] ?? json['category_name_ar'] ?? json['categoryNameAr'] ?? json['cat_name_ar'] ?? json['name_ar'])?.toString(); + countryNameEn = (json['country_name_en'] ?? json['countryNameEn'] ?? json['category_name_en'] ?? json['categoryNameEn'] ?? json['cat_name_en'] ?? json['name_en'])?.toString(); // Use category image URL from HMG middleware using ID from WalaPlus_Categories API: // https://epharmacy.hmg.com/middleware/images/category/{id}.png diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart index 69842263..054d7412 100644 --- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -68,6 +68,23 @@ class WalaPlusRetailersByIDResponse { factory WalaPlusRetailersByIDResponse.fromJson(Map json) { List locList = WalaPlusRetailerModel.parseLocationsList(json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']); List offList = WalaPlusRetailerModel.parseOffersList(json['offers'] ?? json['Offers']); + List catList = []; + dynamic catData = json['categories'] ?? json['Categories'] ?? json['category'] ?? json['Category']; + if (catData is List) { + for (var c in catData) { + if (c is Map) { + catList.add(WalaPlusCategoryModel.fromJson(c)); + } else if (c is Map) { + catList.add(WalaPlusCategoryModel.fromJson(Map.from(c))); + } else if (c is String && c.trim().isNotEmpty) { + catList.add(WalaPlusCategoryModel(name: c.trim())); + } + } + } else if (catData is Map) { + catList.add(WalaPlusCategoryModel.fromJson(Map.from(catData))); + } else if (catData is String && catData.trim().isNotEmpty) { + catList.add(WalaPlusCategoryModel(name: catData.trim())); + } List retList = []; dynamic retData = json['retailer'] ?? json['Retailer']; @@ -77,6 +94,7 @@ class WalaPlusRetailersByIDResponse { var model = WalaPlusRetailerModel.fromJson(r); if (model.locations == null || model.locations!.isEmpty) model.locations = locList; if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); } @@ -89,6 +107,7 @@ class WalaPlusRetailersByIDResponse { var model = WalaPlusRetailerModel.fromJson(Map.from(r)); if (model.locations == null || model.locations!.isEmpty) model.locations = locList; if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); } @@ -103,6 +122,7 @@ class WalaPlusRetailersByIDResponse { var model = WalaPlusRetailerModel.fromJson(Map.from(retData)); if (model.locations == null || model.locations!.isEmpty) model.locations = locList; if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); } @@ -174,6 +194,8 @@ class WalaPlusRetailerModel { List? offers; List? locations; List? categories; + String? categoryId; + String? categoryName; String? validity; String? validTo; String? validUntil; @@ -213,6 +235,8 @@ class WalaPlusRetailerModel { this.offers, this.locations, this.categories, + this.categoryId, + this.categoryName, this.validity, this.validTo, this.validUntil, @@ -297,10 +321,79 @@ class WalaPlusRetailerModel { List locationsList = parseLocationsList(locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']); List categoriesList = []; - if (targetJson['categories'] != null && targetJson['categories'] is List) { - targetJson['categories'].forEach((v) { - categoriesList.add(WalaPlusCategoryModel.fromJson(v)); - }); + dynamic rawCats = targetJson['categories'] ?? targetJson['Categories'] ?? targetJson['category'] ?? targetJson['Category']; + if (rawCats != null && rawCats is List) { + for (var v in rawCats) { + if (v is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(v)); + } else if (v is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(Map.from(v))); + } else if (v is String && v.trim().isNotEmpty) { + categoriesList.add(WalaPlusCategoryModel(name: v.trim())); + } + } + } else if (rawCats is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(Map.from(rawCats))); + } else if (rawCats is String && rawCats.toString().trim().isNotEmpty && rawCats.toString().trim() != "null") { + categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim())); + } + + String? singleCatName = (targetJson['category_name'] ?? + targetJson['categoryName'] ?? + targetJson['cat_name'] ?? + targetJson['catName'] ?? + targetJson['CategoryName'] ?? + targetJson['Category_Name'] ?? + targetJson['category_title'] ?? + targetJson['categoryTitle'])?.toString().trim(); + String? singleCatAr = (targetJson['category_name_ar'] ?? + targetJson['categoryNameAr'] ?? + targetJson['cat_name_ar'] ?? + targetJson['catNameAr'] ?? + targetJson['category_ar'] ?? + targetJson['country_name_ar'] ?? + targetJson['countryNameAr'])?.toString().trim(); + String? singleCatEn = (targetJson['category_name_en'] ?? + targetJson['categoryNameEn'] ?? + targetJson['cat_name_en'] ?? + targetJson['catNameEn'] ?? + targetJson['category_en'] ?? + targetJson['country_name_en'] ?? + targetJson['countryNameEn'])?.toString().trim(); + String? foundCategoryId = (targetJson['category_id'] ?? + targetJson['categoryId'] ?? + targetJson['cat_id'] ?? + targetJson['catId'] ?? + targetJson['WP_CategoryID'] ?? + targetJson['WP_CategoryId'])?.toString().trim(); + + String? rawType = targetJson['type']?.toString().trim(); + String? lowerType = rawType?.toLowerCase(); + bool isTypeStoreKeyword = lowerType == null || + lowerType.isEmpty || + lowerType == 'online' || + lowerType == 'instore' || + lowerType == 'in_store' || + lowerType == 'in store' || + lowerType == 'store' || + lowerType == 'online,instore' || + lowerType == 'instore,online'; + + if ((singleCatName == null || singleCatName.isEmpty) && !isTypeStoreKeyword && rawType != null && rawType.isNotEmpty) { + singleCatName = rawType; + } + + if (categoriesList.isEmpty) { + if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") || + (singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") || + (singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) { + categoriesList.add(WalaPlusCategoryModel( + id: foundCategoryId, + name: singleCatName ?? singleCatEn ?? singleCatAr, + countryNameAr: singleCatAr ?? singleCatName, + countryNameEn: singleCatEn ?? singleCatName, + )); + } } WalaPlusRetailerContact? contactObj; @@ -424,6 +517,8 @@ class WalaPlusRetailerModel { offers: offersList, locations: locationsList, categories: categoriesList, + categoryId: foundCategoryId, + categoryName: singleCatName ?? singleCatEn ?? singleCatAr, validity: foundValidity, validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(), validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(), @@ -528,9 +623,50 @@ class WalaPlusRetailerModel { if (categories != null) { data['categories'] = categories!.map((v) => v.toJson()).toList(); } + data['category_id'] = categoryId; + data['category_name'] = categoryName; return data; } + String? getDisplayCategory({bool isAr = false, List? fallbackCategories}) { + if (categories != null && categories!.isNotEmpty) { + for (var cat in categories!) { + String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; + String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); + String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null") + ? primary.trim() + : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + if (resolved != null && resolved.isNotEmpty) { + return resolved; + } + } + } + if (categoryName != null && categoryName!.trim().isNotEmpty && categoryName != "null") { + return categoryName!.trim(); + } + if (fallbackCategories != null && categoryId != null && categoryId!.trim().isNotEmpty) { + for (var cat in fallbackCategories) { + if (cat.id?.toString() == categoryId?.toString()) { + String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; + String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); + String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null") + ? primary.trim() + : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + if (resolved != null && resolved.isNotEmpty) { + return resolved; + } + } + } + } + if (type != null && type!.trim().isNotEmpty) { + String lt = type!.trim().toLowerCase(); + if (lt != 'online' && lt != 'instore' && lt != 'store' && lt != 'in_store') { + return type!.trim(); + } + } + return null; + } + String? get displayImage { if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim(); if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim(); @@ -688,8 +824,8 @@ class WalaPlusRetailerModel { // this splits and normalizes every part instead of only ever matching a single exact value, // so a combined retailer is correctly recognized as both rather than neither/just one. List get normalizedTypes { + Set result = {}; if (type != null && type!.trim().isNotEmpty) { - Set result = {}; for (String part in type!.split(',')) { String t = part.trim().toLowerCase(); if (t.isEmpty) continue; @@ -698,15 +834,19 @@ class WalaPlusRetailerModel { } else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') { result.add('instore'); } else { - result.add(t); + // Non-online types (e.g. category names like "Physiotherapy") represent + // in-store merchants/services that require physical presence. + result.add('instore'); } } - if (result.isNotEmpty) return result.toList(); } if (locations != null && locations!.isNotEmpty) { - return ['instore']; + result.add('instore'); } - return []; + if (result.isEmpty && hasPhysicalLocation) { + result.add('instore'); + } + return result.toList(); } bool get isOnlineStore => normalizedTypes.contains('online'); @@ -721,14 +861,12 @@ class WalaPlusRetailerModel { String _displayTypeLabel(String norm) { if (norm == 'online') return LocaleKeys.online.tr(); - if (norm == 'instore') return LocaleKeys.inStore.tr(); - return norm; + return LocaleKeys.inStore.tr(); } String? getDisplayType() { List types = normalizedTypes; if (types.isNotEmpty) return _displayTypeLabel(types.first); - if (type != null && type!.trim().isNotEmpty) return type!.trim(); return null; } @@ -737,7 +875,6 @@ class WalaPlusRetailerModel { List getDisplayTypes() { List types = normalizedTypes; if (types.isNotEmpty) return types.map(_displayTypeLabel).toList(); - if (type != null && type!.trim().isNotEmpty) return [type!.trim()]; return []; } } diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index 57070ad6..2da332c4 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -15,6 +15,7 @@ import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/main.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.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'; import 'package:mohem_flutter_app/models/post_params_model.dart'; @@ -36,8 +37,11 @@ class _DashboardScreenForDependetState extends State List retailersList = []; List _filteredRetailersList = []; List categoriesList = []; + WalaPlusCategoryModel? selectedCategory; String currentCategoryId = ""; String selectedTypeFilter = "all"; + String selectedCity = "Riyadh"; + bool _isInitRouteArgs = false; // Subcategories of whichever main category is currently selected - fetched via // getWalaPlusCategories(parentId: ...) whenever a main category card is tapped. Empty/cleared @@ -80,11 +84,26 @@ class _DashboardScreenForDependetState extends State await _saveDependentSessionToPrefs(); await _registerMobileLoginInfo(); await fetchCategories(); - await fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId); } }); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_isInitRouteArgs) { + _isInitRouteArgs = true; + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is WalaPlusCategoryModel) { + selectedCategory = args; + currentCategoryId = args.id ?? ""; + } else if (args is Map && args['category'] is WalaPlusCategoryModel) { + selectedCategory = args['category'] as WalaPlusCategoryModel; + currentCategoryId = selectedCategory?.id ?? ""; + } + } + } + Future _registerMobileLoginInfo() async { if (_hasRegisteredMobileLoginInfo) return; if (!ApiConsts.isDependetLogin) return; @@ -178,19 +197,38 @@ class _DashboardScreenForDependetState extends State } if (mounted) { - List fullList = []; - bool hasAll = list.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل")); - if (!hasAll) { - fullList.add(WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr())); - } - fullList.addAll(list); + categoriesList = list; - setState(() { - categoriesList = fullList; - if (currentCategoryId.isEmpty) { - currentCategoryId = "all"; + if (selectedCategory != null) { + for (WalaPlusCategoryModel cat in list) { + if (cat.id == selectedCategory!.id) { + selectedCategory = cat; + break; + } } - }); + } else { + WalaPlusCategoryModel? defaultCat; + for (WalaPlusCategoryModel cat in list) { + String n = (cat.name ?? "").toLowerCase(); + if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) { + defaultCat = cat; + break; + } + } + defaultCat ??= list.firstWhere( + (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع", + orElse: () => list.isNotEmpty ? list.first : WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment"), + ); + selectedCategory = defaultCat; + } + + currentCategoryId = selectedCategory?.id ?? ""; + setState(() {}); + + if (currentCategoryId.isNotEmpty) { + await fetchSubcategories(currentCategoryId); + await fetchOffers(categoryId: currentCategoryId); + } } } catch (e) { debugPrint("Error fetching WalaPlus categories (attempt 1): $e"); @@ -198,31 +236,48 @@ class _DashboardScreenForDependetState extends State await Future.delayed(const Duration(milliseconds: 600)); List retryList = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); if (mounted && retryList.isNotEmpty) { - List fullList = []; - bool hasAll = retryList.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل")); - if (!hasAll) { - fullList.add(WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr())); - } - fullList.addAll(retryList); - setState(() { - categoriesList = fullList; - if (currentCategoryId.isEmpty) { - currentCategoryId = "all"; + categoriesList = retryList; + if (selectedCategory != null) { + for (WalaPlusCategoryModel cat in retryList) { + if (cat.id == selectedCategory!.id) { + selectedCategory = cat; + break; + } } - }); + } else { + WalaPlusCategoryModel? defaultCat; + for (WalaPlusCategoryModel cat in retryList) { + String n = (cat.name ?? "").toLowerCase(); + if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) { + defaultCat = cat; + break; + } + } + defaultCat ??= retryList.firstWhere( + (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع", + orElse: () => retryList.first, + ); + selectedCategory = defaultCat; + } + currentCategoryId = selectedCategory?.id ?? ""; + setState(() {}); + if (currentCategoryId.isNotEmpty) { + await fetchSubcategories(currentCategoryId); + await fetchOffers(categoryId: currentCategoryId); + } return; } } catch (retryEx) { debugPrint("Error fetching WalaPlus categories (attempt 2): $retryEx"); } - if (mounted && categoriesList.isEmpty) { - setState(() { - categoriesList = [WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr())]; - if (currentCategoryId.isEmpty) { - currentCategoryId = "all"; - } - }); + if (mounted) { + if (selectedCategory == null) { + selectedCategory = WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment"); + currentCategoryId = "1"; + } + setState(() {}); + await fetchOffers(categoryId: currentCategoryId); } } finally { if (mounted) { @@ -272,8 +327,16 @@ class _DashboardScreenForDependetState extends State } } + String? _getUserReferenceForFavorites() { + String ref = OffersAndDiscountsApiClient().resolveUserReference(); + if (ref.trim().isNotEmpty) return ref.trim(); + String? userName = AppState().getUserName; + if (userName != null && userName.trim().isNotEmpty) return userName.trim(); + return null; + } + Future fetchOffers({String? categoryId, String? query}) async { - String targetCategoryId = categoryId ?? (currentCategoryId.isEmpty ? "all" : currentCategoryId); + String targetCategoryId = categoryId ?? _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId); if (query != null) { searchQuery = query; } @@ -305,12 +368,8 @@ class _DashboardScreenForDependetState extends State sortBy: _filterSortBy, radius: radiusParam, minDiscount: minDiscountParam, - // _getCurrentPositionSilently (triggered whenever context is non-null) isn't actually - // silent - it calls Geolocator.requestPermission() if permission hasn't been - // granted/denied yet, which pops the real OS permission dialog. Only pass context when - // Nearest is actually selected, otherwise every fetch (including the default newest-sort - // load on app start) would trigger that prompt for no reason. context: wantsNearest ? context : null, + userReference: null, ); retailersList = response?.retailers ?? []; @@ -346,16 +405,12 @@ class _DashboardScreenForDependetState extends State try { int nextPage = currentPage + 1; - // Must match fetchOffers's category resolution exactly (including the selected - // subcategory, if any) - otherwise pagination silently switches filters mid-scroll, - // fetching an overlapping result set under the same page number and duplicating items. String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId; String catParam = (effectiveCategoryId == "all" || effectiveCategoryId == "0") ? "" : effectiveCategoryId; String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter); String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; int? radiusParam = _radiusFromFilterDistance(_filterDistance); int? minDiscountParam = (_filterMinDiscount == 'any') ? null : int.tryParse(_filterMinDiscount); - print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'"); WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( @@ -368,6 +423,7 @@ class _DashboardScreenForDependetState extends State radius: radiusParam, minDiscount: minDiscountParam, context: wantsNearest ? context : null, + userReference: null, ); List newRetailers = response?.retailers ?? []; @@ -393,8 +449,11 @@ class _DashboardScreenForDependetState extends State } Future _onRefresh() async { - String effectiveCategoryId = _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? "all" : currentCategoryId); - await Future.wait(>[fetchCategories(), fetchOffers(categoryId: effectiveCategoryId, query: _searchController.text)]); + String effectiveCategoryId = _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId); + await Future.wait(>[ + fetchCategories(), + fetchOffers(categoryId: effectiveCategoryId, query: _searchController.text), + ]); } // '5km'/'10km'/'25km' -> 5/10/25 for WP_Radius; 'any' -> null (omit the param - no distance cap). @@ -453,16 +512,89 @@ class _DashboardScreenForDependetState extends State void _onSearchChanged(String query) { _debounceTimer?.cancel(); searchQuery = query; + String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId; if (query.trim().isEmpty) { - fetchOffers(query: ""); + fetchOffers(categoryId: effectiveCategoryId, query: ""); } else { - _debounceTimer = Timer(const Duration(milliseconds: 1000), () { - fetchOffers(query: query.trim()); + _debounceTimer = Timer(const Duration(milliseconds: 600), () { + fetchOffers(categoryId: effectiveCategoryId, query: query.trim()); }); } } + Future _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? false; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + }); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg; + if (isAr) { + msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr(); + } else { + msg = response.wpFavoritesAddRemoveMessage ?? ""; + if (msg.trim().isEmpty) { + msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr(); + } + } + Utils.showToast(msg, isSuccess: true); + } else { + if (mounted) { + setState(() { + item.isFavorite = currentFav; + }); + } + String error = response?.errorEndUserMessage ?? response?.errorMessage ?? LocaleKeys.favoriteError.tr(); + Utils.showToast(error); + } + } catch (e) { + debugPrint("Error toggling favorite: $e"); + if (mounted) { + setState(() { + item.isFavorite = currentFav; + }); + } + Utils.showToast(LocaleKeys.favoriteError.tr()); + } + } + void _openRetailer(WalaPlusRetailerModel item) async { + if (item.categories == null || item.categories!.isEmpty) { + if (selectedCategory != null) { + item.categories = [selectedCategory!]; + } else if (currentCategoryId.isNotEmpty && currentCategoryId != "all") { + for (var c in categoriesList) { + if (c.id?.toString() == currentCategoryId) { + item.categories = [c]; + break; + } + } + } + } await Navigator.pushNamed(context, AppRoutes.walaPlusRetailerDetails, arguments: item); if (mounted) { setState(() {}); @@ -596,70 +728,41 @@ class _DashboardScreenForDependetState extends State title: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!ApiConsts.isDependetLogin) ...[ - InkWell( - borderRadius: BorderRadius.circular(10), - onTap: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } else { - _performLogout(); - } - }, - child: const Padding(padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), - ), - const SizedBox(width: 8), - ], - SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 40, fit: BoxFit.contain), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (ApiConsts.isDependetLogin) _buildLanguageIconButton(isAr), - if (!ApiConsts.isDependetLogin) ...[ - const SizedBox(width: 4), - Tooltip( - message: LocaleKeys.addDependents.tr(), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - Navigator.pushNamed(context, AppRoutes.familyMembers, arguments: {'fromMazaya': true}); - }, - child: Padding( - padding: const EdgeInsets.all(3), - child: SvgPicture.asset('assets/icons/ic_dependents_top.svg', width: 24, height: 24, colorFilter: const ColorFilter.mode(Color(0xFF1E293B), BlendMode.srcIn)), - ), - ), - ), - ], - const SizedBox(width: 4), - Tooltip( - message: LocaleKeys.favorite.tr(), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: _showFavoritesBottomSheet, - child: const Padding(padding: EdgeInsets.all(3), child: Icon(Icons.favorite_outline_rounded, size: 24, color: Colors.black)), - ), - ), - if (ApiConsts.isDependetLogin) ...[ - const SizedBox(width: 4), - InkWell( - borderRadius: BorderRadius.circular(8), - onTap: _performLogout, - child: Padding(padding: const EdgeInsets.all(3), child: SvgPicture.asset('assets/images/drawer/logout.svg', height: 20, width: 20)), - ), - ], - ], + InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else if (ApiConsts.isDependetLogin) { + _performLogout(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B)), + ), ), + const SizedBox(width: 8), + LocaleKeys.mazayaDeals.tr().toText22(isBold: true, color: const Color(0xFF202F31)), ], ), ), + actions: [ + if (ApiConsts.isDependetLogin) ...[ + _buildLanguageIconButton(isAr), + const SizedBox(width: 4), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _performLogout, + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset('assets/images/drawer/logout.svg', height: 20, width: 20), + ), + ), + const SizedBox(width: 12), + ], + ], ), body: SafeArea( child: RefreshIndicator( @@ -672,45 +775,33 @@ class _DashboardScreenForDependetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Welcome greeting - same "Welcome Back" label + large bold name design used - // on the last-login screen, shown for dependent logins only. - if (ApiConsts.isDependetLogin && (AppState().dependentName ?? "").isNotEmpty) ...[ - LocaleKeys.welcomeBack.tr().toText12(), - _cleanDependentName(AppState().dependentName!).toText18(isBold: true), - const SizedBox(height: 16), - ], - - // Search Input Card - _buildSearchBar(isAr), + // 1. Sub-header: "Explore your benefits" | "📍 Riyadh" + _buildSubHeaderRow(isAr), - // "Browse Categories" Header - _buildBrowseCategoriesHeader(isAr), + // 2. Category Hero Section: Title, Subtitle, Category image + _buildCategoryHero(isAr), - // Categories Section - if (categoriesList.isNotEmpty) _buildCategoriesList(isAr) else _buildCategoriesShimmer(), + // 3. Search Bar: "Find an escape or an experience" + _buildSearchBar(isAr), - // Type Filter Chips (All, In Store, Online) - _buildFiltersRow(isAr), + // 4. Filters & Subcategories Row + _buildSubcategoriesAndFilterRow(isAr), const SizedBox(height: 16), - // Offers Grid + // 5. Offers Grid if (isLoading) _buildRetailersShimmer() else if (_filteredRetailersList.isNotEmpty) ...[ GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - // Fixed pixel height instead of childAspectRatio - the card's content is - // fixed in absolute pixels, not proportional to screen width, so an aspect - // ratio made the computed cell height inconsistent across device sizes. - // _buildOfferCard now also standardizes every card's content height (the - // subtitle line and badges row are always reserved/fixed-height, not - // conditional/wrapping), so 235 is a real, stable fit - not a worst-case - // guess - with a non-scrollable SingleChildScrollView backstop in the card - // itself for the rare case a device still needs a bit more (e.g. large - // system text scaling), which clips instead of overflowing. - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isTablet ? 4 : 2, + mainAxisExtent: 235, + crossAxisSpacing: 12, + mainAxisSpacing: 16, + ), itemCount: _filteredRetailersList.length, itemBuilder: (context, index) { return _buildOfferCard(_filteredRetailersList[index], isAr); @@ -721,7 +812,10 @@ class _DashboardScreenForDependetState extends State Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), ], ] else - Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))), + Padding( + padding: const EdgeInsets.only(top: 40), + child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B))), + ), ], ), ), @@ -730,125 +824,174 @@ class _DashboardScreenForDependetState extends State ); } - Widget _buildSearchBar(bool isAr) { - return Container( - width: double.infinity, - height: 70, - padding: const EdgeInsets.only(top: 14, right: 16, bottom: 14, left: 16), - decoration: BoxDecoration( - color: const Color(0xFFFFFFFF), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0), - boxShadow: [BoxShadow(color: const Color(0xFF000000).withOpacity(0.0314), offset: const Offset(0, 2), blurRadius: 5, spreadRadius: 0)], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + Widget _buildSubHeaderRow(bool isAr) { + return Padding( + padding: const EdgeInsets.only(top: 4, bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.search.tr().toText12(isBold: true, color: const Color(0xFF2A333C)), - const SizedBox(height: 2), - TextField( - controller: _searchController, - onChanged: _onSearchChanged, - onSubmitted: (String val) { - _debounceTimer?.cancel(); - fetchOffers(query: val.trim()); - }, - textInputAction: TextInputAction.search, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.zero, - hintText: LocaleKeys.searchItems.tr(), - hintStyle: const TextStyle(color: Color(0xFFACAFB1), fontSize: 14, fontWeight: FontWeight.w400, height: 1.2), - border: InputBorder.none, - // Listens to the controller directly (rather than relying on _onSearchChanged's - // own setState elsewhere) so the clear icon reliably shows/hides on every - // keystroke, independent of the search debounce/fetch timing. - suffixIcon: ValueListenableBuilder( - valueListenable: _searchController, - builder: (BuildContext context, TextEditingValue value, Widget? child) { - if (value.text.isEmpty) return const SizedBox.shrink(); - return InkWell( - child: const Icon(Icons.clear, size: 18, color: Color(0xFF848486)), - // padding: EdgeInsets.zero, - // constraints: const BoxConstraints(), - onTap: () { - _searchController.clear(); - _onSearchChanged(""); - }, - ); - }, + LocaleKeys.exploreYourBenefits.tr().toText13(color: const Color(0xFF79807F)), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _showCitySelectionBottomSheet, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_on_outlined, size: 16, color: Color(0xFF157D6B)), + const SizedBox(width: 4), + selectedCity.toText13(isBold: true, color: const Color(0xFF157D6B)), + ], ), - // max, not min - this search bar's outer Container is a fixed height (70), so the - // icon must never be forced taller than the TextField's own natural height or it - // overflows the available space. - suffixIconConstraints: const BoxConstraints(maxWidth: 24, maxHeight: 28), ), - style: const TextStyle(color: Color(0xFF2A333C), fontSize: 14, fontWeight: FontWeight.w400), ), ], ), ); } - Widget _buildBrowseCategoriesHeader(bool isAr) { - return Padding(padding: const EdgeInsets.only(top: 22, bottom: 14), child: LocaleKeys.browseCategories.tr().toText18(isBold: true, color: const Color(0xFF1E293B))); + Widget _buildCategoryHero(bool isAr) { + String categoryName = selectedCategory?.name ?? (isAr ? "السفر والترفيه" : "Travel & Entertainment"); + String subtitle = isAr ? "مغامرتك القادمة، مع القليل من التميز." : "Your next adventure, with a little extra."; + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + categoryName.toText24(isBold: true, color: const Color(0xFF202F31)), + const SizedBox(height: 6), + subtitle.toText12(color: const Color(0xFF7A8180)), + ], + ), + ), + const SizedBox(width: 16), + _buildCategoryHeroImage(selectedCategory), + ], + ), + ); + } + + Widget _buildCategoryHeroImage(WalaPlusCategoryModel? category) { + const double imgWidth = 84.0; + const double imgHeight = 84.0; + const double radius = 18.0; + + String? imgUrl = category?.categoryImageUrl; + if (imgUrl != null && imgUrl.isNotEmpty) { + if (imgUrl.toLowerCase().endsWith('.svg') || (category?.isSvg ?? false)) { + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: imgWidth, + height: imgHeight, + child: SvgPicture.network( + imgUrl, + fit: BoxFit.cover, + placeholderBuilder: (_) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius), + ), + ), + ); + } + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: imgWidth, + height: imgHeight, + child: Image.network( + imgUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius), + ), + ), + ); + } + return _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius); } - Widget _buildCategoriesList(bool isAr) { - return SizedBox( - height: 136.0, + Widget _buildCategoryHeroFallback(WalaPlusCategoryModel? category, double w, double h, double r) { + return ClipRRect( + borderRadius: BorderRadius.circular(r), + child: Container( + width: w, + height: h, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF38BDF8), Color(0xFF0284C7)], + ), + ), + child: const Center( + child: Icon(Icons.flight_takeoff_rounded, size: 36, color: Colors.white), + ), + ), + ); + } + + Widget _buildSearchBar(bool isAr) { + return Container( width: double.infinity, - child: ListView.separated( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: categoriesList.length, - separatorBuilder: (_, __) => const SizedBox(width: 14), - itemBuilder: (BuildContext context, int index) { - WalaPlusCategoryModel item = categoriesList[index]; - bool isSelected = (item.id == "all" && (currentCategoryId == "all" || currentCategoryId.isEmpty)) || (currentCategoryId.isNotEmpty && currentCategoryId == item.id); - String title = (item.id == "all") ? LocaleKeys.allCategories.tr() : (item.name ?? ""); - - return InkWell( - borderRadius: BorderRadius.circular(18), - onTap: () { - if (currentCategoryId != item.id) { - setState(() { - currentCategoryId = item.id ?? ""; - }); - fetchOffers(categoryId: currentCategoryId); - fetchSubcategories(currentCategoryId); - } - }, - child: SizedBox( - width: 84, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildCategoryCard(item, isSelected), - const SizedBox(height: 8), - title.toText13( - // isBold: true, - color: const Color(0xFF1E293B), - maxlines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + height: 46, + padding: const EdgeInsets.symmetric(horizontal: 14), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE8EAE6), width: 1.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.search_rounded, size: 20, color: Color(0xFF79807F)), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + onChanged: _onSearchChanged, + onSubmitted: (String val) { + _debounceTimer?.cancel(); + fetchOffers(query: val.trim()); + }, + textInputAction: TextInputAction.search, + textAlignVertical: TextAlignVertical.center, + style: const TextStyle(color: Color(0xFF202F31), fontSize: 13, fontWeight: FontWeight.w400), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.findAnEscapeOrAnExperience.tr(), + hintStyle: const TextStyle(color: Color(0xFF7A8180), fontSize: 13, fontWeight: FontWeight.w400), + border: InputBorder.none, ), ), - ); - }, + ), + ValueListenableBuilder( + valueListenable: _searchController, + builder: (BuildContext context, TextEditingValue value, Widget? child) { + if (value.text.isEmpty) return const SizedBox.shrink(); + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + _searchController.clear(); + _onSearchChanged(""); + }, + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.clear, size: 18, color: Color(0xFF848486)), + ), + ); + }, + ), + ], ), ); } - // Replaces the old All/In Store/Online chip row: a fixed "Filters" chip (opens the filters - // bottom sheet) + a divider, then a horizontally scrollable row of subcategory chips fetched - // from the API for whichever main category is currently selected (see fetchSubcategories). - // How many of the 4 filter sections are away from their default value - // (Availability: all, Sort by: newest, Distance: any, Minimum discount: any). int _activeFilterCount() { int count = 0; if (selectedTypeFilter != 'all') count++; @@ -858,9 +1001,7 @@ class _DashboardScreenForDependetState extends State return count; } - Widget _buildFiltersRow(bool isAr) { - // "All Categories" (or nothing selected) has no subcategories to show at all - only a - // specific main category, once it has actually loaded some, gets the divider + chip row. + Widget _buildSubcategoriesAndFilterRow(bool isAr) { bool showSubcategoriesRow = currentCategoryId.isNotEmpty && currentCategoryId != "all" && (isSubcategoriesLoading || subcategoriesList.isNotEmpty); int activeFilterCount = _activeFilterCount(); @@ -892,8 +1033,8 @@ class _DashboardScreenForDependetState extends State ), if (activeFilterCount > 0) PositionedDirectional( - top: -6, - end: -6, + top: -5, + end: -5, child: Container( width: 18, height: 18, @@ -923,8 +1064,6 @@ class _DashboardScreenForDependetState extends State : ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), - // +1 for the leading "All" chip (clears the subcategory selection, - // falling back to just the parent main category). itemCount: subcategoriesList.length + 1, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8), itemBuilder: (BuildContext context, int index) { @@ -939,8 +1078,6 @@ class _DashboardScreenForDependetState extends State setState(() { _selectedSubcategoryId = isAllChip ? null : (isSelected ? null : subcategory!.id); }); - // Selecting a subcategory filters offers to just that subcategory; - // "All" (or deselecting) falls back to the parent main category. fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId); }, child: Container( @@ -960,6 +1097,73 @@ class _DashboardScreenForDependetState extends State ); } + void _showCitySelectionBottomSheet() { + List cities = [ + LocaleKeys.riyadh.tr(), + AppState().isArabic(context) ? "جدة" : "Jeddah", + AppState().isArabic(context) ? "الدمام" : "Dammam", + AppState().isArabic(context) ? "الخبر" : "Khobar", + AppState().isArabic(context) ? "مكة المكرمة" : "Mecca", + AppState().isArabic(context) ? "المدينة المنورة" : "Medina", + ]; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2)), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)), + InkWell( + onTap: () => Navigator.pop(sheetContext), + borderRadius: BorderRadius.circular(16), + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B)), + ), + ), + ], + ), + const SizedBox(height: 12), + ...cities.map((city) { + bool isSelected = selectedCity == city; + return ListTile( + title: city.toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)), + trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null, + onTap: () { + setState(() { + selectedCity = city; + }); + Navigator.pop(sheetContext); + }, + ); + }).toList(), + ], + ), + ), + ); + }, + ); + } + // Matches the shared Filters bottom sheet design. All four sections are fully wired to the // API: Availability -> WP_Offer_Type (via selectedTypeFilter), Sort by -> WP_Sort_By, Distance // -> WP_Radius, Minimum discount -> WP_Min_Discount. "Any"/"All" maps to null (param omitted). @@ -1144,7 +1348,7 @@ class _DashboardScreenForDependetState extends State _filterMinDiscount = draftMinDiscount; }); Navigator.pop(sheetContext); - fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId); + fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId); }), ), ], @@ -1159,14 +1363,14 @@ class _DashboardScreenForDependetState extends State ); } - void _showFavoritesBottomSheet() async { + void showFavoritesBottomSheet() async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, elevation: 0, builder: (BuildContext sheetContext) { - return const FavoriteMerchantsBottomSheet(); + return FavoriteMerchantsBottomSheet(categoriesList: categoriesList); }, ); if (mounted) { @@ -1174,145 +1378,18 @@ class _DashboardScreenForDependetState extends State } } - Widget _buildCategoryCard(WalaPlusCategoryModel item, bool isSelected) { - const double cardSize = 84.0; - const double borderRadiusVal = 18.0; - const double borderWidthVal = 2.8; - const Color activeBorderColor = Color(0xFF00A86B); - - bool isAll = (item.id == "all" || item.id == "0" || (item.name ?? "").toLowerCase() == "all" || item.name == "الجميع" || item.name == "الكل"); - - Widget imageContent; - - if (isAll) { - imageContent = Image.asset('assets/images/cat_all.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item)); - } else { - String categoryImageUrl = item.categoryImageUrl.isNotEmpty ? item.categoryImageUrl : (item.image?.trim() ?? ""); - - if (categoryImageUrl.isNotEmpty) { - if (categoryImageUrl.toLowerCase().endsWith('.svg') || item.isSvg) { - imageContent = SvgPicture.network( - categoryImageUrl, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - placeholderBuilder: (_) => const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00A86B)))), - ); - } else { - imageContent = Image.network(categoryImageUrl, width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item)); - } - } else { - imageContent = _getFallbackCategoryAsset(item); - } - } - - return Container( - width: cardSize, - height: cardSize, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(borderRadiusVal), - border: isSelected ? Border.all(color: activeBorderColor, width: borderWidthVal) : null, - ), - child: ClipRRect(borderRadius: BorderRadius.circular(isSelected ? (borderRadiusVal - borderWidthVal) : borderRadiusVal), child: imageContent), - ); - } - - Widget _getFallbackCategoryAsset(WalaPlusCategoryModel item) { - String name = (item.name ?? "").toLowerCase(); - - if (item.id == "all" || item.id == "0" || name == "all" || name == "الكل" || name == "الجميع") { - return Image.asset('assets/images/cat_all.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item)); - } else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) { - return Image.asset('assets/images/cat_retail.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item)); - } else if (name.contains("food") || name.contains("restaurant") || name.contains("مطاعم") || name.contains("أغذية") || name.contains("cafe") || name.contains("مقهى")) { - return Image.asset('assets/images/cat_food.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item)); - } else if (name.contains("fitness") || name.contains("wellness") || name.contains("sport") || name.contains("gym") || name.contains("رياضة") || name.contains("لياقة") || name.contains("صحة")) { - return Image.asset('assets/images/cat_fitness.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item)); - } - - return _buildFallbackCategoryIconWidget(item); - } - - Widget _buildFallbackCategoryIconWidget(WalaPlusCategoryModel item) { - return Container(color: Colors.transparent, alignment: Alignment.center, child: _getFallbackCategoryIcon(item, const Color(0xFF168A89))); - } - - Widget _getFallbackCategoryIcon(WalaPlusCategoryModel item, Color iconColor) { - String name = (item.name ?? "").toLowerCase(); - IconData iconData = Icons.grid_view_rounded; - Color color = iconColor; - if (item.id == "all" || name == "all" || name == "الكل" || name == "الجميع") { - iconData = Icons.confirmation_number_outlined; - color = const Color(0xFFE52332); - } else if (name.contains("finance") || name.contains("بنوك") || name.contains("مالي")) { - iconData = Icons.account_balance_rounded; - } else if (name.contains("entertain") || name.contains("activities") || name.contains("أنشطة") || name.contains("ترفيه")) { - iconData = Icons.theater_comedy_rounded; - } else if (name.contains("educat") || name.contains("تعليم")) { - iconData = Icons.menu_book_rounded; - } else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) { - iconData = Icons.shopping_bag_outlined; - } else if (name.contains("health") || name.contains("medical") || name.contains("صحة") || name.contains("طبي")) { - iconData = Icons.health_and_safety_outlined; - } else if (name.contains("beauty") || name.contains("جمال") || name.contains("عناية")) { - iconData = Icons.face_retouching_natural_rounded; - } else if (name.contains("fitness") || name.contains("sport") || name.contains("gym") || name.contains("رياضة") || name.contains("لياقة")) { - iconData = Icons.fitness_center_rounded; - } else if (name.contains("hotel") || name.contains("travel") || name.contains("فندق") || name.contains("فنادق") || name.contains("سفر")) { - iconData = Icons.hotel_rounded; - } else if (name.contains("service") || name.contains("خدمات")) { - iconData = Icons.build_circle_outlined; - } else if (name.contains("food") || name.contains("restaurant") || name.contains("مطاعم") || name.contains("أغذية")) { - iconData = Icons.restaurant_rounded; - } else if (name.contains("home") || name.contains("منزل")) { - iconData = Icons.home_rounded; - } - - return Icon(iconData, color: color, size: 26); - } - Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) { - return WalaPlusRetailerCard(item: item, isAr: isAr, onTap: () => _openRetailer(item)); + return WalaPlusRetailerCard( + item: item, + isAr: isAr, + onTap: () => _openRetailer(item), + onFavoriteTap: () => _toggleFavorite(item), + ); } Widget _buildRetailersShimmer() { return _buildRetailersShimmerWidget(isTablet); } - - Widget _buildCategoriesShimmer() { - return SizedBox( - height: 136.0, - child: Shimmer.fromColors( - baseColor: const Color(0xFFE2E8F0), - highlightColor: const Color(0xFFF8FAFC), - child: ListView.separated( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: 5, - separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 14), - itemBuilder: (BuildContext _, int index) { - return SizedBox( - width: 84, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container(width: 84, height: 84, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(18))), - const SizedBox(height: 8), - Container(width: 65, height: 11, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), - const SizedBox(height: 4), - Container(width: 42, height: 11, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), - ], - ), - ); - }, - ), - ), - ); - } } Widget _buildRetailersShimmerWidget(bool isTablet) { @@ -1356,8 +1433,9 @@ class WalaPlusRetailerCard extends StatelessWidget { final WalaPlusRetailerModel item; final bool isAr; final VoidCallback onTap; + final VoidCallback? onFavoriteTap; - const WalaPlusRetailerCard({Key? key, required this.item, required this.isAr, required this.onTap}) : super(key: key); + const WalaPlusRetailerCard({Key? key, required this.item, required this.isAr, required this.onTap, this.onFavoriteTap}) : super(key: key); static String getDiscountBadge(WalaPlusRetailerModel item) { if (item.offerTitle != null) { @@ -1470,7 +1548,7 @@ class WalaPlusRetailerCard extends StatelessWidget { ), if (discountText.isNotEmpty) PositionedDirectional( - top: 20, + top: 44, end: 0, child: Container( padding: const EdgeInsetsDirectional.only(start: 14, end: 10, top: 4, bottom: 4), @@ -1478,6 +1556,35 @@ class WalaPlusRetailerCard extends StatelessWidget { child: discountText.toText14(isBold: true, color: Colors.white), ), ), + PositionedDirectional( + top: 8, + end: 8, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onFavoriteTap, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Icon( + item.isFavorite == true ? Icons.favorite_rounded : Icons.favorite_border_rounded, + size: 17, + color: item.isFavorite == true ? const Color(0xFFE52332) : const Color(0xFF1E293B), + ), + ), + ), + ), PositionedDirectional( bottom: 8, start: 8, @@ -1553,7 +1660,8 @@ class WalaPlusRetailerCard extends StatelessWidget { } class FavoriteMerchantsBottomSheet extends StatefulWidget { - const FavoriteMerchantsBottomSheet({Key? key}) : super(key: key); + final List? categoriesList; + const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList}) : super(key: key); @override State createState() => _FavoriteMerchantsBottomSheetState(); @@ -1724,7 +1832,61 @@ class _FavoriteMerchantsBottomSheetState extends State _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? true; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + if (!targetFav) { + favoriteRetailers.removeWhere((r) => r.id == item.id); + } + }); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg = isAr + ? (targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr()) + : (response.wpFavoritesAddRemoveMessage ?? (targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr())); + Utils.showToast(msg, isSuccess: true); + } else { + await fetchFavorites(query: _searchController.text); + } + } catch (e) { + debugPrint("Error toggling favorite in bottom sheet: $e"); + await fetchFavorites(query: _searchController.text); + } + } + void _openRetailer(WalaPlusRetailerModel item) async { + if ((item.categories == null || item.categories!.isEmpty) && widget.categoriesList != null && item.categoryId != null) { + for (var c in widget.categoriesList!) { + if (c.id?.toString() == item.categoryId?.toString()) { + item.categories = [c]; + break; + } + } + } await Navigator.pushNamed(context, AppRoutes.walaPlusRetailerDetails, arguments: item); if (mounted) { await fetchFavorites(query: _searchController.text); @@ -1761,20 +1923,12 @@ class _FavoriteMerchantsBottomSheetState extends State Navigator.pop(context), - // child: const Padding(padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), - // ), - // const SizedBox(width: 8), LocaleKeys.favoriteOffers.tr().toText18(weight: FontWeight.w600, color: const Color(0xFF1E293B)), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ - // const Icon(Icons.favorite_rounded, size: 22, color: Colors.black), - // const SizedBox(width: 10), InkWell( borderRadius: BorderRadius.circular(16), onTap: () => Navigator.pop(context), @@ -1864,7 +2018,12 @@ class _FavoriteMerchantsBottomSheetState extends State _openRetailer(favoriteRetailers[index])); + return WalaPlusRetailerCard( + item: favoriteRetailers[index], + isAr: isAr, + onTap: () => _openRetailer(favoriteRetailers[index]), + onFavoriteTap: () => _toggleFavorite(favoriteRetailers[index]), + ); }, ), if (isLoadingMore) ...[ diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/landing/wala_plus_retailer_details_screen.dart index 54268323..5ac27929 100644 --- a/lib/ui/landing/wala_plus_retailer_details_screen.dart +++ b/lib/ui/landing/wala_plus_retailer_details_screen.dart @@ -159,6 +159,9 @@ class _WalaPlusRetailerDetailsScreenState extends State(Color(0xFF168A89))), - Expanded( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 14, offset: const Offset(0, 4))], - ), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 1. Top Banner Image with rounded corners (tappable to open merchant website) - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: _openMerchantWebsite, - child: SizedBox( - height: 190, - width: double.infinity, - child: - (_retailer?.displayImage != null && _retailer!.displayImage!.isNotEmpty) - ? Image.network( - _retailer!.displayImage!, - 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, + body: (_isLoading && !_hasDetailsData) + ? _buildDetailsShimmer() + : Column( + children: [ + if (_isLoading && _hasDetailsData) + const LinearProgressIndicator( + minHeight: 2.5, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation(Color(0xFF168A89)), + ), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Breadcrumbs (Category / Company Name) + _buildBreadcrumbs(isAr), + 12.height, - // 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib) - _buildBrandStrip(isAr), - 16.height, + // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container) + _buildHeroCard(isAr), + 14.height, - // 3. Retailer Name & Favorite Button - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(child: (_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B))), - const SizedBox(width: 12), - Container( - width: 44, - height: 44, - decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFFF1F5F9)), - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: Icon(_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, size: 22, color: _isFavorite ? Colors.black : const Color(0xFF1E293B)), - tooltip: LocaleKeys.favorite.tr(), - onPressed: _isTogglingFavorite ? null : _toggleFavorite, - ), - ), - ], - ), - 12.height, - - // 4. Info Badges Row (Store Type, City Chips) - _buildInfoBadgesRow(isAr), - - // 5. Visit Merchant Website Link - if (_retailer?.resolvedWebsite != null && _retailer!.resolvedWebsite!.trim().isNotEmpty) ...[ - 14.height, - InkWell( - onTap: _openMerchantWebsite, - child: LocaleKeys.visitMerchantWebsite.tr().toText14(color: const Color(0xFF0D9488), isUnderLine: true, weight: FontWeight.w600), - ), - ], - 10.height, - - // 5. Description Section (parsed as HTML) - // "${LocaleKeys.description.tr()}:".toText16(isBold: true, color: const Color(0xFF1E293B)), - // 8.height, - // Html( - // data: _getHowToUseHtml(isAr), - // style: {"body": Style(fontSize: FontSize(14.0), color: const Color(0xFF4B5563))}, - // onLinkTap: (String? url, Map attributes, _) async { - // if (url != null && url.isNotEmpty) { - // try { - // Uri uri = Uri.parse(url); - // if (!uri.hasScheme) { - // uri = Uri.parse("https://$url"); - // } - // await launchUrl(uri, mode: LaunchMode.externalApplication); - // } catch (e) { - // debugPrint("Error launching url: $e"); - // } - // } - // }, - // ), - ], - ), - ), + // 3. Store Type & City Filter Chips + _buildChipsRow(isAr), + 20.height, - // 6. Locations Section (below description) - _buildLocationsSection(isAr), + // 4. Merchant Contact (Website, Phone, Email) + _buildMerchantContactSection(isAr), - // 7. Contact Information (Email & Phone Number) - _buildContactSection(isAr), + // 5. Locations Section (Preview Cards + View All) + _buildLocationsSection(isAr), - 16.height, - ], - ), + // 6. Good to Know Section + _buildGoodToKnowSection(isAr), + + 20.height, + ], ), ), + ), - // 7. Bottom Action Button - Container( - padding: const EdgeInsets.fromLTRB(16, 10, 16, 20), - color: Colors.white, - child: SizedBox(width: double.infinity, height: 50, child: DefaultButton(LocaleKeys.useOffer.tr(), _isRedeeming ? null : _onUseOffer, height: 50, borderRadius: 12, fontSize: 16)), - ), - ], - ), + // 7. Bottom Redeem Offer Action Bar + _buildBottomActionBar(isAr), + ], + ), ); } - // Skeleton mirroring the real card layout (banner, brand strip, name, badges) plus two - // generic placeholder cards for the locations/contact sections below it. + // Skeleton mirroring the real card layout (breadcrumbs, hero card, chips, contact, locations) Widget _buildDetailsShimmer() { Widget block({double? width, double height = 14, double radius = 4}) { return Container(width: width, height: height, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(radius))); @@ -753,59 +664,177 @@ class _WalaPlusRetailerDetailsScreenState extends State[ + // Breadcrumbs placeholder + block(width: 180, height: 14), + 14.height, + // Hero card placeholder Container( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), - padding: const EdgeInsets.all(16), + clipBehavior: Clip.antiAlias, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Banner - block(width: double.infinity, height: 190, radius: 16), - 14.height, - // Brand strip - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [block(width: 36, height: 36, radius: 18), const SizedBox(width: 10), block(width: 90, height: 14)]), - block(width: 48, height: 48, radius: 10), - ], + children: [ + block(width: double.infinity, height: 200, radius: 0), + Container( + width: double.infinity, + color: const Color(0xFFCBD5E1), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + block(width: 160, height: 20), + 8.height, + block(width: 220, height: 16), + 10.height, + block(width: 120, height: 12), + ], + ), ), - 18.height, - // Retailer name - block(width: 180, height: 20), - 18.height, - // Info badges row - Row(children: [block(width: 90, height: 28, radius: 8), const SizedBox(width: 8), block(width: 70, height: 28, radius: 8)]), ], ), ), 16.height, - // Locations section placeholder - Container( + // Chips row placeholder + Row(children: [ + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 100, height: 32, radius: 20), + ]), + 20.height, + // Merchant contact placeholder + block(width: 140, height: 18), + 12.height, + block(width: double.infinity, height: 120, radius: 16), + 20.height, + // Locations placeholder + block(width: 100, height: 18), + 12.height, + block(width: double.infinity, height: 68, radius: 16), + 10.height, + block(width: double.infinity, height: 68, radius: 16), + ], + ), + ), + ); + } + + Widget _buildBreadcrumbs(bool isAr) { + String? categoryName = _retailer?.getDisplayCategory(isAr: isAr, fallbackCategories: OffersAndDiscountsApiClient.cachedCategories); + String retailerName = _retailer?.name ?? ''; + + return Row( + children: [ + if (categoryName != null && categoryName.isNotEmpty) ...[ + categoryName.toText13(color: const Color(0xFF0D9488), isBold: true), + " / ".toText13(color: const Color(0xFF94A3B8)), + ], + retailerName.toText13(color: const Color(0xFF64748B), isBold: false).expanded, + ], + ); + } + + Widget _buildHeroCard(bool isAr) { + String? categoryName = _retailer?.getDisplayCategory(isAr: isAr, fallbackCategories: OffersAndDiscountsApiClient.cachedCategories); + String displayCity = _retailer?.getDisplayCity(isAr: isAr) ?? ''; + String subtitleText = ''; + if (categoryName != null && categoryName.isNotEmpty && displayCity.isNotEmpty) { + subtitleText = '$categoryName · $displayCity'; + } else if (categoryName != null && categoryName.isNotEmpty) { + subtitleText = categoryName; + } else if (displayCity.isNotEmpty) { + subtitleText = displayCity; + } + + String offerTitle = _retailer?.offerTitle ?? (_retailer?.offers != null && _retailer!.offers!.isNotEmpty ? (_retailer!.offers!.first.title ?? '') : ''); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow(color: const Color(0xff000000).withOpacity(.08), blurRadius: 16, offset: const Offset(0, 4)), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Image Banner with Favorite Heart Button + SizedBox( + height: 200, width: double.infinity, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [block(width: 120, height: 16), 14.height, block(width: double.infinity, height: 56, radius: 12), 10.height, block(width: double.infinity, height: 56, radius: 12)], + child: Stack( + fit: StackFit.expand, + children: [ + InkWell( + onTap: _openMerchantWebsite, + child: (_retailer?.displayImage != null && _retailer!.displayImage!.isNotEmpty) + ? Image.network( + _retailer!.displayImage!, + 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)), + ), + ), + PositionedDirectional( + top: 14, + end: 14, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.14), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + _isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, + size: 22, + color: _isFavorite ? Colors.black : const Color(0xFF1E293B), + ), + tooltip: LocaleKeys.favorite.tr(), + onPressed: _isTogglingFavorite ? null : _toggleFavorite, + ), + ), + ), + ], ), ), - 16.height, - // Contact section placeholder + + // Bottom #143F36 Dark Green Container Container( width: double.infinity, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), - padding: const EdgeInsets.all(16), + color: const Color(0xFF143F36), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - block(width: 120, height: 16), - 14.height, - Row(children: [block(width: 32, height: 32, radius: 16), const SizedBox(width: 10), block(width: 160, height: 14)]), + children: [ + (_retailer?.name ?? "").toText22(isBold: true, color: Colors.white, isCentered: false), + if (offerTitle.trim().isNotEmpty) ...[ + 6.height, + offerTitle.trim().toText16(isBold: false, color: Colors.white, isCentered: false, maxlines: 2), + ], + if (subtitleText.isNotEmpty) ...[ + 8.height, + subtitleText.toText12(color: Colors.white.withOpacity(0.85), isCenter: false), + ], ], ), ), @@ -815,223 +844,341 @@ class _WalaPlusRetailerDetailsScreenState extends State[ - // Left: Retailer Logo - if (logoUrl != null && logoUrl.isNotEmpty) - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.04), blurRadius: 4, offset: const Offset(0, 2))], - ), - padding: const EdgeInsets.all(2), - child: Image.network(logoUrl, height: 36, width: 36, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36)), - ) - else - const SizedBox(width: 36, height: 36), - - // Right: Mazaya Brand Logo - SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 48, fit: BoxFit.contain), - ], - ); - } + Widget _buildChipsRow(bool isAr) { + List chips = []; - Widget _buildInfoBadgesRow(bool isAr) { - // type can be a comma-separated combo (e.g. "online,instore") - normalizedTypes splits - // that into its distinct parts, so a retailer offering both ways gets a chip each, same - // as the dashboard card. - List offerTypes = _retailer?.normalizedTypes ?? []; - // Single aggregated chip - same as the dashboard card: the actual city when there's only - // one, or the localized "Multi-city" label when locations span more than one distinct city. - String cityText = _retailer?.getDisplayCity(isAr: isAr) ?? ''; - - return Wrap( - spacing: 8, - runSpacing: 8, - children: [ - // One Type Chip per distinct type - Online (green) and/or In Store (blue). - for (String offerType in offerTypes) - InfoChipWidget( - text: - offerType == 'online' - ? LocaleKeys.online.tr() - : (offerType == 'instore' ? LocaleKeys.inStore.tr() : (offerType.isNotEmpty ? (offerType[0].toUpperCase() + offerType.substring(1)) : offerType)), - icon: offerType == 'online' ? Icons.language_rounded : Icons.storefront_outlined, - backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF), - textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB), - borderColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF3B82F6), - horizontalPadding: 10, - verticalPadding: 5, - borderRadius: 12, - iconSize: 14, - ), - if (cityText.trim().isNotEmpty) - InfoChipWidget( - text: cityText, - icon: Icons.location_on_outlined, - backgroundColor: const Color(0xFFF8FAFC), - textColor: const Color(0xFF475569), - borderColor: const Color(0xFFCBD5E1), - horizontalPadding: 10, - verticalPadding: 5, - borderRadius: 12, - iconSize: 14, - ), - ], - ); - } + List types = _retailer?.normalizedTypes ?? []; + bool hasOnline = types.contains('online') || (_retailer?.isOnlineStore == true); + bool hasInstore = types.contains('instore') || (_retailer?.isInstore == true) || (_retailer?.hasPhysicalLocation == true) || (_retailer?.locations != null && _retailer!.locations!.isNotEmpty); - String _getHowToUseHtml(bool isAr) { - String? content; + String rType = (_retailer?.type ?? '').toLowerCase(); + String rOfferType = (_retailer?.offerType ?? '').toLowerCase(); + if (rType.contains('online') || rOfferType.contains('online')) hasOnline = true; + if (rType.contains('store') || rOfferType.contains('store')) hasInstore = true; + if (rType.isNotEmpty && !rType.contains('online')) hasInstore = true; - // 1. Language-specific how-to-use field - if (isAr) { - if (_retailer?.arHowToUse != null && _retailer!.arHowToUse!.trim().isNotEmpty && _retailer!.arHowToUse != "null") { - content = _retailer!.arHowToUse!.trim(); - } - } else { - if (_retailer?.enHowToUse != null && _retailer!.enHowToUse!.trim().isNotEmpty && _retailer!.enHowToUse != "null") { - content = _retailer!.enHowToUse!.trim(); + if (_retailer?.offers != null) { + for (var offer in _retailer!.offers!) { + String oType = (offer.type ?? '').toLowerCase(); + if (oType.contains('online')) hasOnline = true; + if (oType.contains('store') || (oType.isNotEmpty && !oType.contains('online'))) hasInstore = true; } } - - // 2. In WalaPlus API, 'usage' field often holds the rich HTML formatted instructions - if (content == null || content.isEmpty) { - if (_retailer?.usage != null) { - String u = _retailer!.usage.toString().trim(); - if (u.isNotEmpty && u != "null") { - // If usage contains HTML tags or step keywords, prioritize it - if (u.contains('<') || u.toLowerCase().contains('step') || u.contains('خطوة') || u.toLowerCase().contains('discount')) { - content = u; - } - } - } + if (!hasOnline && !hasInstore) { + hasInstore = true; } - // 3. Fallback to generic 'howToUse' - if (content == null || content.isEmpty) { - if (_retailer?.howToUse != null && _retailer!.howToUse!.trim().isNotEmpty && _retailer!.howToUse != "null") { - content = _retailer!.howToUse!.trim(); - } + // Online chip + if (hasOnline) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFA7F3D0)), + ), + child: LocaleKeys.online.tr().toText12(color: const Color(0xFF00A651), isBold: true), + ), + ); } - // 4. Fallback to 'usage' even if plain text - if (content == null || content.isEmpty) { - if (_retailer?.usage != null) { - String u = _retailer!.usage.toString().trim(); - if (u.isNotEmpty && u != "null" && int.tryParse(u) == null) { - content = u; - } - } + // In Store chip + if (hasInstore) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFBFDBFE)), + ), + child: LocaleKeys.inStore.tr().toText12(color: const Color(0xFF2563EB), isBold: true), + ), + ); } - // 5. Fallback to the other language field - if (content == null || content.isEmpty) { - if (isAr) { - if (_retailer?.enHowToUse != null && _retailer!.enHowToUse!.trim().isNotEmpty && _retailer!.enHowToUse != "null") { - content = _retailer!.enHowToUse!.trim(); - } - } else { - if (_retailer?.arHowToUse != null && _retailer!.arHowToUse!.trim().isNotEmpty && _retailer!.arHowToUse != "null") { - content = _retailer!.arHowToUse!.trim(); + // Cities chips + List distinctCities = []; + if (_retailer?.locations != null) { + Set seen = {}; + for (var loc in _retailer!.locations!) { + String c = loc.getLocalizedCity(isAr); + if (c.isEmpty) c = loc.city ?? ''; + if (c.trim().isNotEmpty && !seen.contains(c.trim())) { + seen.add(c.trim()); + distinctCities.add(c.trim()); } } } - // 6. Fallback to description or info - if (content == null || content.isEmpty) { - if (_retailer?.description != null && _retailer!.description!.trim().isNotEmpty && _retailer!.description != "null") { - content = _retailer!.description!.trim(); - } else if (_retailer?.info != null && _retailer!.info!.trim().isNotEmpty && _retailer!.info != "null") { - content = _retailer!.info!.trim(); + if (distinctCities.length <= 2) { + for (String c in distinctCities) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: c.toText12(color: const Color(0xFF334155), isBold: false), + ), + ); } + } else { + for (int i = 0; i < 2; i++) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: distinctCities[i].toText12(color: const Color(0xFF334155), isBold: false), + ), + ); + } + chips.add( + InkWell( + onTap: () => _showAllLocationsBottomSheet(_retailer!.locations!, isAr), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: LocaleKeys.moreCitiesCount.tr(args: ['${distinctCities.length - 2}']).toText12(color: const Color(0xFF334155), isBold: false), + ), + ), + ); } - if (content == null || content.isEmpty) { - return "

${LocaleKeys.noInstructionsAvailable.tr()}

"; + if (chips.isEmpty) { + return const SizedBox.shrink(); } - return _formatHtmlInstructions(content); + return Wrap( + spacing: 8, + runSpacing: 8, + children: chips, + ); } - String _formatHtmlInstructions(String raw) { - String s = raw.trim(); - - // 1. Strip surrounding quotes (handles multiple layers e.g. "\"text\"") - while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - if (s.length >= 2) { - s = s.substring(1, s.length - 1).trim(); - } else { - break; - } - } - - // 2. Unescape JSON slashes & quotes - s = s.replaceAll(r'\/', '/').replaceAll(r'\"', '"').replaceAll(r"\'", "'"); - - // 3. Decode \uXXXX Unicode escape sequences - s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) { - try { - int codeUnit = int.parse(match.group(1)!, radix: 16); - return String.fromCharCode(codeUnit); - } catch (_) { - return match.group(0)!; - } - }); - - // 4. Decode common specific unicode escapes (quotes, bullets, dashes, etc.) - s = s - .replaceAll(r'\u201c', '“') - .replaceAll(r'\u201d', '”') - .replaceAll(r'\u2019', '’') - .replaceAll(r'\u2018', '‘') - .replaceAll(r'\u00a0', ' ') - .replaceAll(r'\u2022', '•') - .replaceAll(r'\u2013', '–') - .replaceAll(r'\u2014', '—') - .replaceAll(r'\u200f', '') - .replaceAll(r'\u200e', '') - .replaceAll(r'\u061f', '؟') - .replaceAll(r'\u201', '“'); // Fallback if truncated to 3 digits - - // 5. Decode HTML entities - s = s.replaceAll('"', '"').replaceAll(''', "'").replaceAll(''', "'").replaceAll(' ', ' ').replaceAll('&', '&'); - - // 6. Check if string contains HTML tags - bool hasHtmlTags = RegExp(r'<[a-zA-Z][^>]*>').hasMatch(s); - - if (!hasHtmlTags) { - // Plain text: format nicely into structured HTML with bold step titles and line breaks - s = s.replaceAll(r'\r\n', '\n').replaceAll(r'\n', '\n'); - - // Separate step markers - RegExp stepRegex = RegExp( - r'(\b(?:First|Second|Third|Fourth|Fifth|Sixth|Seventh|Eighth|Ninth|Tenth)\s+step\s*:?|' - r'\b(?:1st|2nd|3rd|4th|5th|6th|7th|8th|9th|10th)\s+step\s*:?|' - r'(?:الخطوة\s+(?:الأولى|الثانية|الثالثة|الرابعة|الخامسة|السادسة|السابعة|الثامنة|التاسعة|العاشرة))\s*:?)', - caseSensitive: false, - ); + Widget _buildMerchantContactSection(bool isAr) { + String? website = _retailer?.resolvedWebsite; + String? phone = _retailer?.resolvedPhone; + String? email = _retailer?.resolvedEmail; - s = s.replaceAllMapped(stepRegex, (match) { - String matchText = match.group(0)!.trim(); - return '

$matchText '; - }); + bool hasWebsite = website != null && website.trim().isNotEmpty && website != "null"; + bool hasPhone = phone != null && phone.trim().isNotEmpty && phone != "null"; + bool hasEmail = email != null && email.trim().isNotEmpty && email != "null"; - // Split remaining newlines to
- s = s.replaceAll('\n', '
'); + if (!hasWebsite && !hasPhone && !hasEmail) { + return const SizedBox.shrink(); + } - // Clean leading
tags - s = s.replaceFirst(RegExp(r'^(?:)+'), ''); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.merchantContact.tr().toText18(isBold: true, color: const Color(0xFF1E293B)), + 12.height, + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: [ + BoxShadow(color: const Color(0xff000000).withOpacity(.03), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Column( + children: [ + if (hasWebsite) ...[ + InkWell( + onTap: _openMerchantWebsite, + borderRadius: BorderRadius.vertical(top: const Radius.circular(16), bottom: (!hasPhone && !hasEmail) ? const Radius.circular(16) : Radius.zero), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.website.tr().toText12(color: const Color(0xFF64748B)), + 4.height, + website.toText14(color: const Color(0xFF2563EB), isUnderLine: true, weight: FontWeight.w500), + ], + ).expanded, + const Icon(Icons.open_in_new_rounded, size: 20, color: Color(0xFF64748B)), + ], + ), + ), + ), + ], + if (hasPhone) ...[ + if (hasWebsite) + const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), + InkWell( + onTap: () async { + try { + Uri phoneUri = Uri(scheme: 'tel', path: phone); + await launchUrl(phoneUri); + } catch (e) { + debugPrint("Error launching phone: $e"); + } + }, + borderRadius: (!hasWebsite && !hasEmail) ? BorderRadius.circular(16) : (!hasEmail ? const BorderRadius.vertical(bottom: Radius.circular(16)) : BorderRadius.zero), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.phone.tr().toText12(color: const Color(0xFF64748B)), + 4.height, + phone.toText14(color: const Color(0xFF1E293B), weight: FontWeight.w600), + ], + ).expanded, + const Icon(Icons.phone_outlined, size: 20, color: Color(0xFF64748B)), + ], + ), + ), + ), + ], + if (hasEmail) ...[ + if (hasWebsite || hasPhone) + const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), + InkWell( + onTap: () async { + try { + Uri emailUri = Uri(scheme: 'mailto', path: email); + await launchUrl(emailUri); + } catch (e) { + debugPrint("Error launching email: $e"); + } + }, + borderRadius: BorderRadius.vertical(top: (!hasWebsite && !hasPhone) ? const Radius.circular(16) : Radius.zero, bottom: const Radius.circular(16)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr().toText12(color: const Color(0xFF64748B)), + 4.height, + email.toText14(color: const Color(0xFF1E293B), weight: FontWeight.w600), + ], + ).expanded, + const Icon(Icons.mail_outline_rounded, size: 20, color: Color(0xFF64748B)), + ], + ), + ), + ), + ], + ], + ), + ), + 20.height, + ], + ); + } - s = '

$s

'; - } + Widget _buildGoodToKnowSection(bool isAr) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 10.height, + LocaleKeys.goodToKnow.tr().toText18(isBold: true, color: const Color(0xFF1E293B)), + 14.height, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon(Icons.info_outline_rounded, color: Color(0xFF0D9488), size: 20), + ), + 10.width, + LocaleKeys.goodToKnowDesc.tr().toText13(color: const Color(0xFF475569)).expanded, + ], + ), + 16.height, + const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)), + 14.height, + InkWell( + onTap: () { + // Terms & conditions link will be provided later + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.termsAndConditions.tr().toText14(isBold: true, color: const Color(0xFF1E293B)), + const Icon(Icons.open_in_new_rounded, size: 20, color: Color(0xFF64748B)), + ], + ), + ), + 14.height, + const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)), + 14.height, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.haveAQuestionAboutOffer.tr().toText13(color: const Color(0xFF64748B)), + InkWell( + onTap: () { + // Get help link will be provided later + }, + child: LocaleKeys.getHelp.tr().toText13(color: const Color(0xFF0D9488), isBold: true), + ), + ], + ), + ], + ); + } - return s; + Widget _buildBottomActionBar(bool isAr) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + decoration: BoxDecoration( + color: Colors.white, + border: const Border(top: BorderSide(color: Color(0xFFF1F5F9), width: 1)), + boxShadow: [ + BoxShadow(color: const Color(0xff000000).withOpacity(.06), blurRadius: 10, offset: const Offset(0, -2)), + ], + ), + child: SizedBox( + width: double.infinity, + height: 52, + child: Material( + color: _isRedeeming ? const Color(0xFF94A3B8) : const Color(0xFF169E8F), + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: _isRedeeming ? null : _onUseOffer, + borderRadius: BorderRadius.circular(14), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _isRedeeming + ? const Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation(Colors.white)), + ), + ) + : Row( + children: [ + LocaleKeys.redeemOffer.tr().toText16(isBold: true, color: Colors.white, isCentered: false), + const Spacer(), + Icon(isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, color: Colors.white, size: 20), + ], + ), + ), + ), + ), + ), + ); } Widget _buildLocationsSection(bool isAr) { @@ -1046,50 +1193,29 @@ class _WalaPlusRetailerDetailsScreenState extends State 2) - InkWell( - onTap: () => _showAllLocationsBottomSheet(locations, isAr), - borderRadius: BorderRadius.circular(6), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText12(color: const Color(0xFF047857), underlineGap: 1, isUnderLine: true, isBold: false), - // const SizedBox(width: 2), - // Icon(isAr ? Icons.chevron_left_rounded : Icons.chevron_right_rounded, size: 18, color: const Color(0xFF168A89)), - ], - ), + LocaleKeys.locations.tr().toText18(isBold: true, color: const Color(0xFF1E293B)), + InkWell( + onTap: () => _showAllLocationsBottomSheet(locations, isAr), + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.viewAll.tr().toText14(color: const Color(0xFF0D9488), isBold: true), + 4.width, + Icon(isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, size: 16, color: const Color(0xFF0D9488)), + ], ), ), + ), ], ), 12.height, - // Preview Cards ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -1100,43 +1226,7 @@ class _WalaPlusRetailerDetailsScreenState extends State 2) ...[ - // 10.height, - // InkWell( - // onTap: () => _showAllLocationsBottomSheet(locations, isAr), - // borderRadius: BorderRadius.circular(16), - // child: Container( - // padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(16), - // boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))], - // ), - // child: Row( - // children: [ - // Container( - // padding: const EdgeInsets.all(9), - // decoration: BoxDecoration(color: const Color(0xFF168A89).withOpacity(0.08), shape: BoxShape.circle), - // child: const Icon(Icons.map_outlined, color: Color(0xFF168A89), size: 18), - // ), - // 12.width, - // Expanded( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText14(isBold: true, color: const Color(0xFF168A89)), - // 2.height, - // LocaleKeys.browseAllBranches.tr(args: ['$totalCount']).toText11(color: const Color(0xFF64748B)), - // ], - // ), - // ), - // Icon(isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, size: 14, color: const Color(0xFF168A89)), - // ], - // ), - // ), - // ), - // ], + 16.height, ], ); } @@ -1150,98 +1240,47 @@ class _WalaPlusRetailerDetailsScreenState extends State[BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // // City Pill Badge - // if (cityText.isNotEmpty) - // Container( - // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3.5), - // decoration: BoxDecoration(color: const Color(0xFF168A89).withOpacity(0.08), borderRadius: BorderRadius.circular(6)), - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [const Icon(Icons.location_on, size: 12, color: Color(0xFF168A89)), const SizedBox(width: 4), cityText.toText11(isBold: true, color: const Color(0xFF168A89))], - // ), - // ) - // else - // const SizedBox.shrink(), - // ], - // ), - // 10.height, - // Branch title & pin icon - InkWell( - onTap: () => _openMapForLocation(loc), - borderRadius: BorderRadius.circular(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + String? secondaryText = distanceText.isNotEmpty ? distanceText : (addressText.isNotEmpty ? cityText : null); + + return InkWell( + onTap: () => _openMapForLocation(loc), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: [ + BoxShadow(color: const Color(0xff000000).withOpacity(.03), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFF1F5F9), + shape: BoxShape.circle, + ), + child: const Icon(Icons.location_on_outlined, size: 20, color: Color(0xFF64748B)), + ), + 12.width, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(top: 2), - width: 36, - height: 36, - alignment: Alignment.center, - decoration: const BoxDecoration(color: Color(0xFFF3F4F6), shape: BoxShape.circle), - child: SvgPicture.asset("assets/icons/location.svg", width: 16, height: 16), - ), - 8.width, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - primaryTitle.toText16(isBold: false, color: const Color(0xFF202B33), maxlines: 2), - if (secondaryText != null) ...[2.height, secondaryText.toText12(color: const Color(0xFF057958), maxLine: 1)], - ], - ), - ), - ], - ), - ), - - 12.width, - SvgPicture.asset("assets/icons/location.svg", width: 18, height: 18), - - // Container( - // padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - // decoration: BoxDecoration( - // gradient: const LinearGradient( - // transform: GradientRotation(.83), - // begin: Alignment.topRight, - // end: Alignment.bottomLeft, - // colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], - // ), - // borderRadius: BorderRadius.circular(8), - // ), - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [const Icon(Icons.directions_outlined, color: Colors.white, size: 14), const SizedBox(width: 4), LocaleKeys.viewOnMap.tr().toText11(color: Colors.white, isBold: true)], - // ), - // ), + primaryTitle.toText16(isBold: true, color: const Color(0xFF1E293B), maxlines: 1), + if (secondaryText != null && secondaryText.isNotEmpty) ...[ + 3.height, + secondaryText.toText12(color: const Color(0xFF0D9488), weight: FontWeight.w600), + ], ], - ), - ), - ], + ).expanded, + 10.width, + const Icon(Icons.open_in_new_rounded, size: 20, color: Color(0xFF64748B)), + ], + ), ), ); } @@ -1479,118 +1518,4 @@ class _WalaPlusRetailerDetailsScreenState extends State', caseSensitive: false), '\n') - .replaceAll(RegExp(r'

', caseSensitive: false), '\n\n') - .replaceAll(RegExp(r'<[^>]*>'), '') - .replaceAll(' ', ' ') - .replaceAll('&', '&') - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll('<', '<') - .replaceAll('>', '>') - .trim(); - } }