diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 636b0908..e38e3efa 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -887,5 +887,7 @@ "am": "ص", "pm": "م", "carServices": "خدمات السيارات", - "activities": "أنشطة" + "activities": "أنشطة", + "foodAndDrinks": "أطعمة ومشروبات", + "curatedPicksForYourFood": "اختيارات مميزة لأنشطتك." } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 14f083ef..63d4c7e0 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -891,6 +891,8 @@ "initializingChatPleaseWait": "Please wait, initializing chat...", "am": "AM", "pm": "PM", - "activities": "Activities" + "activities": "Activities", + "foodAndDrink": "Food & Drink", + "curatedPicksForYourFood": "Curated picks for your Food & Drink." } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 3f4caa36..4302c5b1 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -899,7 +899,9 @@ class CodegenLoader extends AssetLoader{ "am": "ص", "pm": "م", "carServices": "خدمات السيارات", - "activities": "أنشطة" + "activities": "أنشطة", + "foodAndDrinks": "أطعمة ومشروبات", + "curatedPicksForYourFood": "اختيارات مميزة لأنشطتك." }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1786,7 +1788,9 @@ static const Map _en_US = { "initializingChatPleaseWait": "Please wait, initializing chat...", "am": "AM", "pm": "PM", - "activities": "Activities" + "activities": "Activities", + "foodAndDrink": "Food & Drink", + "curatedPicksForYourFood": "Curated picks for your Food & Drink." }; static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 18a73399..605ae2dd 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -872,5 +872,7 @@ abstract class LocaleKeys { static const pm = 'pm'; static const carServices = 'carServices'; static const activities = 'activities'; + static const foodAndDrinks = 'foodAndDrinks'; + static const curatedPicksForYourFood = 'curatedPicksForYourFood'; } diff --git a/lib/ui/mazaya/mazaya_discover_screen.dart b/lib/ui/mazaya/mazaya_discover_screen.dart index ba6ee051..34c3df41 100644 --- a/lib/ui/mazaya/mazaya_discover_screen.dart +++ b/lib/ui/mazaya/mazaya_discover_screen.dart @@ -23,10 +23,6 @@ import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:shimmer/shimmer.dart'; -// A single, fixed-category brand-list section (Travel Essentials, Home & Kitchen, ...). Swapping -// which category a section shows is a one-line change - just the categoryId passed to the -// constructor where the section is declared - since the fetch, the title resolution, and the UI -// binding all read off this one object instead of each having their own copy of the id. class _MazayaCategorySection { final String categoryId; bool isLoading = true; @@ -39,11 +35,7 @@ class _MazayaCategorySection { _MazayaCategorySection(this.categoryId); } -// Mazaya Discover screen (Figma node 890:2050, "Mazaya Deals - Curated for you"). Reached from -// the Mazaya banner on the main dashboard. Most sections are wired to the WalaPlus APIs -// (categories + per-category retailers). "Exclusive offers" instead reuses the legacy Offers & -// Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) - see -// _fetchExclusiveOffers and WalaPlusRetailerModel.fromExclusiveOffer. + class MazayaDiscoverScreen extends StatefulWidget { const MazayaDiscoverScreen({Key? key}) : super(key: key); @@ -67,8 +59,8 @@ class _MazayaDiscoverScreenState extends State { // Fixed-category sections - to point a section at a different category, change only the id // passed here. Everything else (fetch, title, UI) reads off the _MazayaCategorySection object. final _MazayaCategorySection _travelSection = _MazayaCategorySection('p03'); - final _MazayaCategorySection _activitiesSection = _MazayaCategorySection( - 'jk0', + final _MazayaCategorySection _foodAndDrinksSection = _MazayaCategorySection( + 'bZa', ); @override @@ -99,7 +91,7 @@ class _MazayaDiscoverScreenState extends State { if (mounted) { _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); - _fetchCategorySection(_activitiesSection); + _fetchCategorySection(_foodAndDrinksSection); } } @@ -131,11 +123,6 @@ class _MazayaDiscoverScreenState extends State { } } - // Fetches one fixed-category section (Travel Essentials, Home & Kitchen, ...) by whichever - // categoryId is set on the given _MazayaCategorySection. Title data comes from the first - // matching entry in each returned retailer's own embedded `categories` list (each retailer - // carries its full category info straight from the API) - used as a fallback by _sectionTitle - // when the id isn't found in the top-level categories list. Future _fetchCategorySection(_MazayaCategorySection section) async { setState(() => section.isLoading = true); try { @@ -174,11 +161,6 @@ class _MazayaDiscoverScreenState extends State { } } - // "Latest on Mazaya" = newest retailers across all categories (WP_Sort_By=newest, WP_CategoryID - // omitted entirely - not just empty) - same WP_Sort_By param already wired for the main - // retailers grid's Sort by filter. No category filter here on purpose: the Figma mock itself - // mixes categories in this section (a logistics brand next to a food brand), confirming it's a - // cross-category feed. Future _fetchLatestOnMazaya() async { setState(() => _isLoadingLatest = true); try { @@ -202,11 +184,6 @@ class _MazayaDiscoverScreenState extends State { } } - // Resolved live rather than stored once, since the two sources load independently and either - // can arrive first: the top-level categories list (the primary/most reliable source - present - // even if the section's category currently has zero retailers) and that category's own data - // embedded in each retailer (kept as a fallback for when the id isn't in the top-level list). - // fallbackTitle only shows while neither source has resolved a name yet. String _sectionTitle(_MazayaCategorySection section, String fallbackTitle) { bool isAr = mounted ? AppState().isArabic(context) : false; for (WalaPlusCategoryModel c in _categories) { @@ -225,9 +202,6 @@ class _MazayaDiscoverScreenState extends State { return fallbackTitle; } - // Same id-first/resolvedName-fallback lookup as _sectionTitle, but returns the full category - // model so "See all" can hand RetailersListScreen the actual category to pre-select (it re-syncs - // this against its own category list once that loads, so an id match is all that's required). WalaPlusCategoryModel _categoryForSection( _MazayaCategorySection section, String fallbackTitle, @@ -241,11 +215,6 @@ class _MazayaDiscoverScreenState extends State { ); } - // "Exclusive offers" reuses the legacy Offers & Discounts (ITG) API/implementation - // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the - // same "all categories" value that screen's own initial load uses. Each result is mapped onto - // WalaPlusRetailerModel (see fromExclusiveOffer) so it renders through the same card/grid/detail - // screen as every other Mazaya section, flagged via isExclusive. Future _fetchExclusiveOffers() async { setState(() => _isLoadingExclusiveOffers = true); try { @@ -305,7 +274,7 @@ class _MazayaDiscoverScreenState extends State { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); - _fetchCategorySection(_activitiesSection); + _fetchCategorySection(_foodAndDrinksSection); } } @@ -325,7 +294,7 @@ class _MazayaDiscoverScreenState extends State { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); - _fetchCategorySection(_activitiesSection); + _fetchCategorySection(_foodAndDrinksSection); } } @@ -349,7 +318,7 @@ class _MazayaDiscoverScreenState extends State { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); - _fetchCategorySection(_activitiesSection); + _fetchCategorySection(_foodAndDrinksSection); } } @@ -358,11 +327,43 @@ class _MazayaDiscoverScreenState extends State { _fetchCategories(), _fetchLatestOnMazaya(), _fetchCategorySection(_travelSection), - _fetchCategorySection(_activitiesSection), + _fetchCategorySection(_foodAndDrinksSection), _fetchExclusiveOffers(), ]); } + void _openCitySelector() { + showMazayaCitySelectionBottomSheet( + context, + selectedCityId: selectedCity?.cityId, + onCitySelected: (city) { + WalaPlusCityModel? newCity = city.isAllCities ? null : city; + AppState().setSelectedMazayaCity = newCity; + if (newCity != null) { + Utils.saveStringFromPrefs( + "MAZAYA_SELECTED_CITY", + jsonEncode(newCity.toJson()), + ); + } else { + Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + } + setState(() => selectedCity = newCity); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_foodAndDrinksSection); + }, + ); + } + + void _resetToAllCities() { + AppState().setSelectedMazayaCity = null; + Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + setState(() => selectedCity = null); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_foodAndDrinksSection); + } + Future _clearDependentPrefs() async { await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); @@ -531,39 +532,40 @@ class _MazayaDiscoverScreenState extends State { ), ), ), + _buildBrandSection( title: _sectionTitle( - _travelSection, - LocaleKeys.carServices.tr(), + _foodAndDrinksSection, + LocaleKeys.homeAndKitchen.tr(), ), - subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), - isLoading: _travelSection.isLoading, - retailers: _travelSection.retailers, - backgroundColor: const Color(0xFFEFF2EF), + subtitle: LocaleKeys.curatedPicksForYourFood.tr(), + isLoading: _foodAndDrinksSection.isLoading, + retailers: _foodAndDrinksSection.retailers, + backgroundColor: Colors.white, onSeeAll: () => _openRetailersGrid( category: _categoryForSection( - _travelSection, - LocaleKeys.carServices.tr(), + _foodAndDrinksSection, + LocaleKeys.homeAndKitchen.tr(), ), ), ), _buildBrandSection( title: _sectionTitle( - _activitiesSection, - LocaleKeys.homeAndKitchen.tr(), + _travelSection, + LocaleKeys.carServices.tr(), ), - subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(), - isLoading: _activitiesSection.isLoading, - retailers: _activitiesSection.retailers, - backgroundColor: Colors.white, + subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), + isLoading: _travelSection.isLoading, + retailers: _travelSection.retailers, + backgroundColor: const Color(0xFFEFF2EF), onSeeAll: () => _openRetailersGrid( - category: _categoryForSection( - _activitiesSection, - LocaleKeys.homeAndKitchen.tr(), - ), - ), + category: _categoryForSection( + _travelSection, + LocaleKeys.carServices.tr(), + ), + ), ), const SizedBox(height: 12), ], @@ -630,57 +632,69 @@ class _MazayaDiscoverScreenState extends State { color: const Color(0xFF78817F), weight: FontWeight.w500, ), - InkWell( - borderRadius: BorderRadius.circular(8), - onTap: - () => showMazayaCitySelectionBottomSheet( - context, - selectedCityId: selectedCity?.cityId, - onCitySelected: (city) { - WalaPlusCityModel? newCity = city.isAllCities ? null : city; - AppState().setSelectedMazayaCity = newCity; - if (newCity != null) { - Utils.saveStringFromPrefs( - "MAZAYA_SELECTED_CITY", - jsonEncode(newCity.toJson()), - ); - } else { - Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); - } - setState(() => selectedCity = newCity); - _fetchLatestOnMazaya(); - _fetchCategorySection(_travelSection); - _fetchCategorySection(_activitiesSection); - }, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - "assets/icons/mazaya_map_pin.svg", - width: 12, - height: 12, - colorFilter: const ColorFilter.mode( - Color(0xFF147D6B), - BlendMode.srcIn, - ), - ), - const SizedBox(width: 3), - (selectedCity?.getDisplayName(isAr) ?? - LocaleKeys.allCitiesOption.tr()) - .toText12(color: const Color(0xFF147D6B)), - const SizedBox(width: 3), - SvgPicture.asset( - "assets/icons/mazaya_chevron_down.svg", - width: 12, - height: 12, - colorFilter: const ColorFilter.mode( - Color(0xFF147D6B), - BlendMode.srcIn, + Builder( + builder: (context) { + bool hasSelectedCity = + selectedCity != null && !selectedCity!.isAllCities; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _openCitySelector, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + "assets/icons/mazaya_map_pin.svg", + width: 12, + height: 12, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + const SizedBox(width: 3), + (selectedCity?.getDisplayName(isAr) ?? + LocaleKeys.allCitiesOption.tr()) + .toText12(color: const Color(0xFF147D6B)), + ], + ), ), - ), - ], - ), + const SizedBox(width: 3), + if (hasSelectedCity) + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _resetToAllCities, + child: const Padding( + padding: EdgeInsets.all(2), + child: Icon( + Icons.close_rounded, + size: 14, + color: Color(0xFF147D6B), + ), + ), + ) + else + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _openCitySelector, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: SvgPicture.asset( + "assets/icons/mazaya_chevron_down.svg", + width: 12, + height: 12, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + ), + ), + ], + ); + }, ), ], ), diff --git a/lib/ui/mazaya/retailer_details_screen.dart b/lib/ui/mazaya/retailer_details_screen.dart index 4c448eaf..b8120565 100644 --- a/lib/ui/mazaya/retailer_details_screen.dart +++ b/lib/ui/mazaya/retailer_details_screen.dart @@ -1753,7 +1753,7 @@ class _RetailerDetailsScreenState extends State { } List sortedCities = cityCounts.keys.toList() - ..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0)); + ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); String selectedCity = "ALL"; String searchQuery = ""; diff --git a/lib/ui/mazaya/retailers_list_screen.dart b/lib/ui/mazaya/retailers_list_screen.dart index 7cc266c5..64156e3a 100644 --- a/lib/ui/mazaya/retailers_list_screen.dart +++ b/lib/ui/mazaya/retailers_list_screen.dart @@ -1059,6 +1059,7 @@ class _RetailersListScreenState extends State { } Widget _buildSubHeaderRow(bool isAr) { + bool hasSelectedCity = selectedCity != null && !selectedCity!.isAllCities; return Padding( padding: const EdgeInsets.only(top: 4, bottom: 16), child: Row( @@ -1067,25 +1068,61 @@ class _RetailersListScreenState extends State { 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), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _showCitySelectionBottomSheet, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on_outlined, + size: 16, + color: Color(0xFF157D6B), + ), + const SizedBox(width: 4), + (selectedCity?.getDisplayName(isAr) ?? + LocaleKeys.allCitiesOption.tr()) + .toText13(isBold: true, color: const Color(0xFF157D6B)), + ], ), - const SizedBox(width: 4), - (selectedCity?.getDisplayName(isAr) ?? - LocaleKeys.allCitiesOption.tr()) - .toText13(isBold: true, color: const Color(0xFF157D6B)), - ], - ), + ), + const SizedBox(width: 3), + if (hasSelectedCity) + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _resetToAllCities, + child: const Padding( + padding: EdgeInsets.all(2), + child: Icon( + Icons.close_rounded, + size: 14, + color: Color(0xFF157D6B), + ), + ), + ) + else + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _showCitySelectionBottomSheet, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: SvgPicture.asset( + "assets/icons/mazaya_chevron_down.svg", + width: 12, + height: 12, + colorFilter: const ColorFilter.mode( + Color(0xFF157D6B), + BlendMode.srcIn, + ), + ), + ), + ), + ], ), ), ], @@ -1528,6 +1565,21 @@ class _RetailersListScreenState extends State { ); } + void _resetToAllCities() { + AppState().setSelectedMazayaCity = null; + Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + setState(() => selectedCity = null); + String effectiveCategoryId = + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); + fetchOffers( + categoryId: effectiveCategoryId, + query: _searchController.text, + ); + } + // 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). diff --git a/lib/ui/mazaya/widgets/mazaya_city_selector.dart b/lib/ui/mazaya/widgets/mazaya_city_selector.dart index 41fff782..53dd8076 100644 --- a/lib/ui/mazaya/widgets/mazaya_city_selector.dart +++ b/lib/ui/mazaya/widgets/mazaya_city_selector.dart @@ -53,10 +53,15 @@ class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> { Future _fetchCities() async { try { List cities = await OffersAndDiscountsApiClient().getWalaPlusCities(); - // "All Cities" always first - it's the default selection when nothing else was picked yet. + + bool isAr = mounted ? AppState().isArabic(context) : false; cities.sort((a, b) { - if (a.isAllCities == b.isAllCities) return 0; - return a.isAllCities ? -1 : 1; + if (a.isAllCities && b.isAllCities) return 0; + if (a.isAllCities) return -1; + if (b.isAllCities) return 1; + String nameA = a.getDisplayName(isAr); + String nameB = b.getDisplayName(isAr); + return nameA.toLowerCase().compareTo(nameB.toLowerCase()); }); if (mounted) { setState(() {