master_merge #24

Merged
Haroon6138 merged 2 commits from master_merge into master 12 hours ago

@ -887,5 +887,7 @@
"am": "ص", "am": "ص",
"pm": "م", "pm": "م",
"carServices": "خدمات السيارات", "carServices": "خدمات السيارات",
"activities": "أنشطة" "activities": "أنشطة",
"foodAndDrinks": "أطعمة ومشروبات",
"curatedPicksForYourFood": "اختيارات مميزة لأنشطتك."
} }

@ -891,6 +891,8 @@
"initializingChatPleaseWait": "Please wait, initializing chat...", "initializingChatPleaseWait": "Please wait, initializing chat...",
"am": "AM", "am": "AM",
"pm": "PM", "pm": "PM",
"activities": "Activities" "activities": "Activities",
"foodAndDrink": "Food & Drink",
"curatedPicksForYourFood": "Curated picks for your Food & Drink."
} }

@ -899,7 +899,9 @@ class CodegenLoader extends AssetLoader{
"am": "ص", "am": "ص",
"pm": "م", "pm": "م",
"carServices": "خدمات السيارات", "carServices": "خدمات السيارات",
"activities": "أنشطة" "activities": "أنشطة",
"foodAndDrinks": "أطعمة ومشروبات",
"curatedPicksForYourFood": "اختيارات مميزة لأنشطتك."
}; };
static const Map<String,dynamic> _en_US = { static const Map<String,dynamic> _en_US = {
"mohemm": "Mohemm", "mohemm": "Mohemm",
@ -1786,7 +1788,9 @@ static const Map<String,dynamic> _en_US = {
"initializingChatPleaseWait": "Please wait, initializing chat...", "initializingChatPleaseWait": "Please wait, initializing chat...",
"am": "AM", "am": "AM",
"pm": "PM", "pm": "PM",
"activities": "Activities" "activities": "Activities",
"foodAndDrink": "Food & Drink",
"curatedPicksForYourFood": "Curated picks for your Food & Drink."
}; };
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
} }

@ -872,5 +872,7 @@ abstract class LocaleKeys {
static const pm = 'pm'; static const pm = 'pm';
static const carServices = 'carServices'; static const carServices = 'carServices';
static const activities = 'activities'; static const activities = 'activities';
static const foodAndDrinks = 'foodAndDrinks';
static const curatedPicksForYourFood = 'curatedPicksForYourFood';
} }

@ -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:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:shimmer/shimmer.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 { class _MazayaCategorySection {
final String categoryId; final String categoryId;
bool isLoading = true; bool isLoading = true;
@ -39,11 +35,7 @@ class _MazayaCategorySection {
_MazayaCategorySection(this.categoryId); _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 { class MazayaDiscoverScreen extends StatefulWidget {
const MazayaDiscoverScreen({Key? key}) : super(key: key); const MazayaDiscoverScreen({Key? key}) : super(key: key);
@ -67,8 +59,8 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
// Fixed-category sections - to point a section at a different category, change only the id // 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. // passed here. Everything else (fetch, title, UI) reads off the _MazayaCategorySection object.
final _MazayaCategorySection _travelSection = _MazayaCategorySection('p03'); final _MazayaCategorySection _travelSection = _MazayaCategorySection('p03');
final _MazayaCategorySection _activitiesSection = _MazayaCategorySection( final _MazayaCategorySection _foodAndDrinksSection = _MazayaCategorySection(
'jk0', 'bZa',
); );
@override @override
@ -99,7 +91,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
if (mounted) { if (mounted) {
_fetchLatestOnMazaya(); _fetchLatestOnMazaya();
_fetchCategorySection(_travelSection); _fetchCategorySection(_travelSection);
_fetchCategorySection(_activitiesSection); _fetchCategorySection(_foodAndDrinksSection);
} }
} }
@ -131,11 +123,6 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
} }
} }
// 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<void> _fetchCategorySection(_MazayaCategorySection section) async { Future<void> _fetchCategorySection(_MazayaCategorySection section) async {
setState(() => section.isLoading = true); setState(() => section.isLoading = true);
try { try {
@ -174,11 +161,6 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
} }
} }
// "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<void> _fetchLatestOnMazaya() async { Future<void> _fetchLatestOnMazaya() async {
setState(() => _isLoadingLatest = true); setState(() => _isLoadingLatest = true);
try { try {
@ -202,11 +184,6 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
} }
} }
// 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) { String _sectionTitle(_MazayaCategorySection section, String fallbackTitle) {
bool isAr = mounted ? AppState().isArabic(context) : false; bool isAr = mounted ? AppState().isArabic(context) : false;
for (WalaPlusCategoryModel c in _categories) { for (WalaPlusCategoryModel c in _categories) {
@ -225,9 +202,6 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
return fallbackTitle; 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( WalaPlusCategoryModel _categoryForSection(
_MazayaCategorySection section, _MazayaCategorySection section,
String fallbackTitle, String fallbackTitle,
@ -241,11 +215,6 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
); );
} }
// "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<void> _fetchExclusiveOffers() async { Future<void> _fetchExclusiveOffers() async {
setState(() => _isLoadingExclusiveOffers = true); setState(() => _isLoadingExclusiveOffers = true);
try { try {
@ -305,7 +274,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
setState(() => selectedCity = AppState().selectedMazayaCity); setState(() => selectedCity = AppState().selectedMazayaCity);
_fetchLatestOnMazaya(); _fetchLatestOnMazaya();
_fetchCategorySection(_travelSection); _fetchCategorySection(_travelSection);
_fetchCategorySection(_activitiesSection); _fetchCategorySection(_foodAndDrinksSection);
} }
} }
@ -325,7 +294,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
setState(() => selectedCity = AppState().selectedMazayaCity); setState(() => selectedCity = AppState().selectedMazayaCity);
_fetchLatestOnMazaya(); _fetchLatestOnMazaya();
_fetchCategorySection(_travelSection); _fetchCategorySection(_travelSection);
_fetchCategorySection(_activitiesSection); _fetchCategorySection(_foodAndDrinksSection);
} }
} }
@ -349,7 +318,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
setState(() => selectedCity = AppState().selectedMazayaCity); setState(() => selectedCity = AppState().selectedMazayaCity);
_fetchLatestOnMazaya(); _fetchLatestOnMazaya();
_fetchCategorySection(_travelSection); _fetchCategorySection(_travelSection);
_fetchCategorySection(_activitiesSection); _fetchCategorySection(_foodAndDrinksSection);
} }
} }
@ -358,11 +327,43 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
_fetchCategories(), _fetchCategories(),
_fetchLatestOnMazaya(), _fetchLatestOnMazaya(),
_fetchCategorySection(_travelSection), _fetchCategorySection(_travelSection),
_fetchCategorySection(_activitiesSection), _fetchCategorySection(_foodAndDrinksSection),
_fetchExclusiveOffers(), _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<void> _clearDependentPrefs() async { Future<void> _clearDependentPrefs() async {
await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN");
await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID");
@ -531,39 +532,40 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
), ),
), ),
), ),
_buildBrandSection( _buildBrandSection(
title: _sectionTitle( title: _sectionTitle(
_travelSection, _foodAndDrinksSection,
LocaleKeys.carServices.tr(), LocaleKeys.homeAndKitchen.tr(),
), ),
subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), subtitle: LocaleKeys.curatedPicksForYourFood.tr(),
isLoading: _travelSection.isLoading, isLoading: _foodAndDrinksSection.isLoading,
retailers: _travelSection.retailers, retailers: _foodAndDrinksSection.retailers,
backgroundColor: const Color(0xFFEFF2EF), backgroundColor: Colors.white,
onSeeAll: onSeeAll:
() => _openRetailersGrid( () => _openRetailersGrid(
category: _categoryForSection( category: _categoryForSection(
_travelSection, _foodAndDrinksSection,
LocaleKeys.carServices.tr(), LocaleKeys.homeAndKitchen.tr(),
), ),
), ),
), ),
_buildBrandSection( _buildBrandSection(
title: _sectionTitle( title: _sectionTitle(
_activitiesSection, _travelSection,
LocaleKeys.homeAndKitchen.tr(), LocaleKeys.carServices.tr(),
), ),
subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(), subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(),
isLoading: _activitiesSection.isLoading, isLoading: _travelSection.isLoading,
retailers: _activitiesSection.retailers, retailers: _travelSection.retailers,
backgroundColor: Colors.white, backgroundColor: const Color(0xFFEFF2EF),
onSeeAll: onSeeAll:
() => _openRetailersGrid( () => _openRetailersGrid(
category: _categoryForSection( category: _categoryForSection(
_activitiesSection, _travelSection,
LocaleKeys.homeAndKitchen.tr(), LocaleKeys.carServices.tr(),
), ),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
@ -630,57 +632,69 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
color: const Color(0xFF78817F), color: const Color(0xFF78817F),
weight: FontWeight.w500, weight: FontWeight.w500,
), ),
InkWell( Builder(
borderRadius: BorderRadius.circular(8), builder: (context) {
onTap: bool hasSelectedCity =
() => showMazayaCitySelectionBottomSheet( selectedCity != null && !selectedCity!.isAllCities;
context, return Row(
selectedCityId: selectedCity?.cityId, mainAxisSize: MainAxisSize.min,
onCitySelected: (city) { children: <Widget>[
WalaPlusCityModel? newCity = city.isAllCities ? null : city; InkWell(
AppState().setSelectedMazayaCity = newCity; borderRadius: BorderRadius.circular(8),
if (newCity != null) { onTap: _openCitySelector,
Utils.saveStringFromPrefs( child: Row(
"MAZAYA_SELECTED_CITY", mainAxisSize: MainAxisSize.min,
jsonEncode(newCity.toJson()), children: <Widget>[
); SvgPicture.asset(
} else { "assets/icons/mazaya_map_pin.svg",
Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); width: 12,
} height: 12,
setState(() => selectedCity = newCity); colorFilter: const ColorFilter.mode(
_fetchLatestOnMazaya(); Color(0xFF147D6B),
_fetchCategorySection(_travelSection); BlendMode.srcIn,
_fetchCategorySection(_activitiesSection); ),
}, ),
), const SizedBox(width: 3),
child: Row( (selectedCity?.getDisplayName(isAr) ??
mainAxisSize: MainAxisSize.min, LocaleKeys.allCitiesOption.tr())
children: <Widget>[ .toText12(color: const Color(0xFF147D6B)),
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,
), ),
), 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,
),
),
),
),
],
);
},
), ),
], ],
), ),

@ -1753,7 +1753,7 @@ class _RetailerDetailsScreenState extends State<RetailerDetailsScreen> {
} }
List<String> sortedCities = List<String> sortedCities =
cityCounts.keys.toList() 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 selectedCity = "ALL";
String searchQuery = ""; String searchQuery = "";

@ -1059,6 +1059,7 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
} }
Widget _buildSubHeaderRow(bool isAr) { Widget _buildSubHeaderRow(bool isAr) {
bool hasSelectedCity = selectedCity != null && !selectedCity!.isAllCities;
return Padding( return Padding(
padding: const EdgeInsets.only(top: 4, bottom: 16), padding: const EdgeInsets.only(top: 4, bottom: 16),
child: Row( child: Row(
@ -1067,25 +1068,61 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
LocaleKeys.exploreYourBenefits.tr().toText13( LocaleKeys.exploreYourBenefits.tr().toText13(
color: const Color(0xFF79807F), color: const Color(0xFF79807F),
), ),
InkWell( Padding(
borderRadius: BorderRadius.circular(8), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
onTap: _showCitySelectionBottomSheet, child: Row(
child: Padding( mainAxisSize: MainAxisSize.min,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), children: [
child: Row( InkWell(
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(8),
children: [ onTap: _showCitySelectionBottomSheet,
const Icon( child: Row(
Icons.location_on_outlined, mainAxisSize: MainAxisSize.min,
size: 16, children: [
color: Color(0xFF157D6B), 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) ?? const SizedBox(width: 3),
LocaleKeys.allCitiesOption.tr()) if (hasSelectedCity)
.toText13(isBold: true, color: const Color(0xFF157D6B)), 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<RetailersListScreen> {
); );
} }
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 // 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 // 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). // -> WP_Radius, Minimum discount -> WP_Min_Discount. "Any"/"All" maps to null (param omitted).

@ -53,10 +53,15 @@ class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> {
Future<void> _fetchCities() async { Future<void> _fetchCities() async {
try { try {
List<WalaPlusCityModel> cities = await OffersAndDiscountsApiClient().getWalaPlusCities(); List<WalaPlusCityModel> 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) { cities.sort((a, b) {
if (a.isAllCities == b.isAllCities) return 0; if (a.isAllCities && b.isAllCities) return 0;
return a.isAllCities ? -1 : 1; 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) { if (mounted) {
setState(() { setState(() {

Loading…
Cancel
Save