Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal

* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  made the city search functional
wala_plus_with_dialog_removal
Sultan khan 1 week ago
commit 3cf2c2a4c8

@ -873,5 +873,6 @@
"offersCount": "{} عروض", "offersCount": "{} عروض",
"mazayaExclusive": "حصري لمزايا", "mazayaExclusive": "حصري لمزايا",
"youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.",
"selectCity": "اختر المدينة" "selectCity": "اختر المدينة",
"searchCities": "البحث عن المدن"
} }

@ -877,6 +877,7 @@
"offersCount": "{} offers", "offersCount": "{} offers",
"mazayaExclusive": "MAZAYA EXCLUSIVE", "mazayaExclusive": "MAZAYA EXCLUSIVE",
"youAreAllCaughtUp": "You're all caught up. More little extras soon.", "youAreAllCaughtUp": "You're all caught up. More little extras soon.",
"selectCity": "Select City" "selectCity": "Select City",
"searchCities": "Search cities"
} }

@ -11,6 +11,7 @@ import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; import 'package:mohem_flutter_app/widgets/location/Location.dart' as location;
@ -288,6 +289,58 @@ class OffersAndDiscountsApiClient {
return list; return list;
} }
// Fetched fresh every time the city picker opens (no caching) - see
// mazaya_city_selector.dart. Only P_USER_NAME/MobileNumber (resolved the same way as every
// other WalaPlus call) and WP_Country vary per call; everything else is the shared default
// WalaPlus params/token from _getBaseWalaPlusParams.
Future<List<WalaPlusCityModel>> getWalaPlusCities({
String? mobileNumber,
String? country,
}) async {
String url = ApiConsts.walaPlusCities;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["P_USER_NAME"] = pUserName;
if (pMobileNumber.isNotEmpty) {
postParams["MobileNumber"] = pMobileNumber;
}
postParams["WP_Country"] = country ?? "";
_cleanWalaPlusParams(postParams);
return await ApiClient().postJsonForObject(
(response) {
List<WalaPlusCityModel> cities = [];
if (response != null) {
dynamic data =
response['WalaPlus_Cities'] ?? response['walaPlus_Cities'];
if (data is String) {
try {
data = json.decode(data);
} catch (e) {
debugPrint("Error decoding WalaPlus_Cities: $e");
}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
cities.add(WalaPlusCityModel.fromJson(v));
} else if (v is Map) {
cities.add(
WalaPlusCityModel.fromJson(Map<String, dynamic>.from(v)),
);
}
}
}
}
return cities;
},
url,
postParams,
);
}
// Confirmed with backend team: WP_Offer_Type accepts "online" and "store". // Confirmed with backend team: WP_Offer_Type accepts "online" and "store".
static const String walaPlusOfferTypeOnline = "online"; static const String walaPlusOfferTypeOnline = "online";
static const String walaPlusOfferTypeInStore = "store"; static const String walaPlusOfferTypeInStore = "store";
@ -320,6 +373,10 @@ class OffersAndDiscountsApiClient {
String? sortBy, String? sortBy,
int? radius, int? radius,
int? minDiscount, int? minDiscount,
// Confirmed with backend team: WP_City_ID=3 or WP_City_ID=0 both mean "all cities" (no
// filter) - any other id filters to that specific city. Default null -> 0, same "always
// send a concrete int" contract as WP_Radius/WP_Min_Discount below.
int? cityId,
BuildContext? context, BuildContext? context,
String? mobileNumber, String? mobileNumber,
String? query, String? query,
@ -361,6 +418,7 @@ class OffersAndDiscountsApiClient {
// here) must be sent as a concrete 0, not omitted. // here) must be sent as a concrete 0, not omitted.
postParams["WP_Radius"] = radius ?? 0; postParams["WP_Radius"] = radius ?? 0;
postParams["WP_Min_Discount"] = minDiscount ?? 0; postParams["WP_Min_Discount"] = minDiscount ?? 0;
postParams["WP_City_ID"] = cityId ?? 0;
if (geoLocation != null && geoLocation.isNotEmpty) { if (geoLocation != null && geoLocation.isNotEmpty) {
postParams["WP_Geo_Location"] = geoLocation; postParams["WP_Geo_Location"] = geoLocation;

@ -4,12 +4,12 @@ class ApiConsts {
// static String baseUrl = "http://10.201.214.101:2027"; // Local server // static String baseUrl = "http://10.201.214.101:2027"; // Local server
// static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server
// static String baseUrl = "http://10.201.204.101:2024"; // static String baseUrl = "http://10.201.204.101:2024";
// static String baseUrl = "https://webservices.hmg.com"; // PreProd // static String baseUrl = "https://webservices.hmg.com"; // PreProd
// static String baseUrl = "https://hmgwebservices.com"; // Live server // static String baseUrl = "https://hmgwebservices.com"; // Live server
static String baseUrl = "https://mohemm.hmg.com"; // New Live server // static String baseUrl = "https://mohemm.hmg.com"; // New Live server
static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server
// //
// static String baseUrl = "https://uat.hmgwebservices.com"; // // static String baseUrl = "https://uat.hmgwebservices.com"; //
@ -17,8 +17,10 @@ class ApiConsts {
// static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "http://10.20.200.111:1010/";
// static String baseUrl = "https://mohemm.hmg.com"; // New Live server // static String baseUrl = "https://mohemm.hmg.com"; // New Live server
//static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT
static String eLearningBaseUrl = "https://elearning.hmg.com/moodle/api/"; // Elearning server live static String eLearningBaseUrl =
static String enSATLiveURL = "https://ensat.hmg.com/auth/autologin"; // ENSAT server live "https://elearning.hmg.com/moodle/api/"; // Elearning server live
static String enSATLiveURL =
"https://ensat.hmg.com/auth/autologin"; // ENSAT server live
// static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "http://10.20.200.111:1010/";
@ -29,7 +31,8 @@ class ApiConsts {
static String baseUrlServices = baseUrl + "/Services/"; // server static String baseUrlServices = baseUrl + "/Services/"; // server
// static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server
static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/";
static String authenticationRest = baseUrlServices + "Authentication.svc/REST/"; static String authenticationRest =
baseUrlServices + "Authentication.svc/REST/";
static String erpRest = baseUrlServices + "ERP.svc/REST/"; static String erpRest = baseUrlServices + "ERP.svc/REST/";
static String swpRest = baseUrlServices + "SWP.svc/REST/"; static String swpRest = baseUrlServices + "SWP.svc/REST/";
static String user = baseUrlServices + "api/User/"; static String user = baseUrlServices + "api/User/";
@ -47,7 +50,9 @@ class ApiConsts {
static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem"; static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem";
static String walaPlusFavoritesAdd = erpRest + "WalaPlus_FavoritesAdd"; static String walaPlusFavoritesAdd = erpRest + "WalaPlus_FavoritesAdd";
static String walaPlusFavoritesRemove = erpRest + "WalaPlus_FavoritesRemove"; static String walaPlusFavoritesRemove = erpRest + "WalaPlus_FavoritesRemove";
static String processContactMazayaAccess = erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS"; static String walaPlusCities = erpRest + "WalaPlus_Cities";
static String processContactMazayaAccess =
erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS";
static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect"; static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect";
@ -58,16 +63,24 @@ class ApiConsts {
static String chatHubConnectionUrl = chatServerBaseUrl + "ConnectionChatHub"; static String chatHubConnectionUrl = chatServerBaseUrl + "ConnectionChatHub";
//Groups //Groups
static String getGroupByUserId = chatServerBaseApiUrl + "group/getgroupsbyuserid/"; static String getGroupByUserId =
static String deleteGroup = chatServerBaseApiUrl + "group/updateGroupIsDeleted/"; chatServerBaseApiUrl + "group/getgroupsbyuserid/";
static String updateGroupAdmin = chatServerBaseApiUrl + "group/updateGroupAdmin/"; static String deleteGroup =
static String getGroupChatHistoryAsync = chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/"; chatServerBaseApiUrl + "group/updateGroupIsDeleted/";
static String addGroupsAndUsers = chatServerBaseApiUrl + "group/addgroupandusers/"; static String updateGroupAdmin =
static String updateGroupsAndUsers = chatServerBaseApiUrl + "group/updategroupandusers/"; chatServerBaseApiUrl + "group/updateGroupAdmin/";
static String getGroupChatHistoryAsync =
chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/";
static String addGroupsAndUsers =
chatServerBaseApiUrl + "group/addgroupandusers/";
static String updateGroupsAndUsers =
chatServerBaseApiUrl + "group/updategroupandusers/";
// static String chatSearchMember = chatLoginTokenUrl + "user/"; // static String chatSearchMember = chatLoginTokenUrl + "user/";
static String chatRecentUrl = chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem static String chatRecentUrl =
static String chatSingleUserHistoryUrl = chatServerBaseApiUrl + "UserChatHistory/"; chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem
static String chatSingleUserHistoryUrl =
chatServerBaseApiUrl + "UserChatHistory/";
static String chatMediaImageUploadUrl = chatServerBaseApiUrl + "shared/"; static String chatMediaImageUploadUrl = chatServerBaseApiUrl + "shared/";
static String chatFavUser = chatServerBaseApiUrl + "FavUser/"; static String chatFavUser = chatServerBaseApiUrl + "FavUser/";
static String chatUserImages = chatServerBaseUrl + "empservice/api/employee/"; static String chatUserImages = chatServerBaseUrl + "empservice/api/employee/";
@ -80,25 +93,33 @@ class ApiConsts {
// static String marathonBaseUrl = marathonBaseUrlUAT; // static String marathonBaseUrl = marathonBaseUrlUAT;
static String marathonBaseUrlServices = "https://marathoon.com/service/"; static String marathonBaseUrlServices = "https://marathoon.com/service/";
static String marathonParticipantLoginUrl = marathonBaseUrl + "auth/participantlogin"; static String marathonParticipantLoginUrl =
marathonBaseUrl + "auth/participantlogin";
static String marathonProjectGetUrl = marathonBaseUrl + "Project/Project_Get"; static String marathonProjectGetUrl = marathonBaseUrl + "Project/Project_Get";
static String marathonUpcomingUrl = marathonBaseUrl + "marathon/upcoming/"; static String marathonUpcomingUrl = marathonBaseUrl + "marathon/upcoming/";
static String marathonJoinParticipantUrl = marathonBaseUrl + "participant/join"; static String marathonJoinParticipantUrl =
marathonBaseUrl + "participant/join";
static String marathonNextQuestionUrl = marathonBaseUrl + "question/next"; static String marathonNextQuestionUrl = marathonBaseUrl + "question/next";
static String marathonSubmitAnswerUrl = marathonBaseUrl + "question/submit"; static String marathonSubmitAnswerUrl = marathonBaseUrl + "question/submit";
static String marathonQualifiersUrl = marathonBaseUrl + "winner/getWinner/"; static String marathonQualifiersUrl = marathonBaseUrl + "winner/getWinner/";
static String marathonSelectedWinner = marathonBaseUrl + "winner/getSelectedWinner/"; static String marathonSelectedWinner =
static String marathonGetMarathonersCount = marathonBaseUrl + "Participant/GetRemainingParticipants"; marathonBaseUrl + "winner/getSelectedWinner/";
static String marathonGetTutorial = marathonBaseUrl + "tutorial/GetTutorialNotification"; static String marathonGetMarathonersCount =
marathonBaseUrl + "Participant/GetRemainingParticipants";
static String marathonGetTutorial =
marathonBaseUrl + "tutorial/GetTutorialNotification";
//DummyCards for the UI //DummyCards for the UI
static int tabletMinLength = 500; static int tabletMinLength = 500;
// Notification Center GUIDs for OTP // Notification Center GUIDs for OTP
static String notificationCenterAppGUID = "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0"; static String notificationCenterAppGUID =
static String notificationCenterLoginServiceGUID = "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2"; "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0";
static String notificationCenterForgotPasswordServiceGUID = "15AA0C70-8DA3-430B-8881-EAFBB508F296"; static String notificationCenterLoginServiceGUID =
"D8BD5C76-4B50-4122-81D0-C3AA25DB08A2";
static String notificationCenterForgotPasswordServiceGUID =
"15AA0C70-8DA3-430B-8881-EAFBB508F296";
static MemberType selectedMemberType = MemberType.employee; static MemberType selectedMemberType = MemberType.employee;

@ -924,6 +924,7 @@ class CodegenLoader extends AssetLoader {
"mazayaExclusive": "حصري لمزايا", "mazayaExclusive": "حصري لمزايا",
"youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.",
"selectCity": "اختر المدينة", "selectCity": "اختر المدينة",
"searchCities": "البحث عن المدن",
}; };
static const Map<String, dynamic> _en_US = { static const Map<String, dynamic> _en_US = {
"mohemm": "Mohemm", "mohemm": "Mohemm",
@ -1848,6 +1849,7 @@ class CodegenLoader extends AssetLoader {
"mazayaExclusive": "MAZAYA EXCLUSIVE", "mazayaExclusive": "MAZAYA EXCLUSIVE",
"youAreAllCaughtUp": "You're all caught up. More little extras soon.", "youAreAllCaughtUp": "You're all caught up. More little extras soon.",
"selectCity": "Select City", "selectCity": "Select City",
"searchCities": "Search cities",
}; };
static const Map<String, Map<String, dynamic>> mapLocales = { static const Map<String, Map<String, dynamic>> mapLocales = {
"ar_SA": _ar_SA, "ar_SA": _ar_SA,

@ -888,4 +888,5 @@ abstract class LocaleKeys {
static const mazayaExclusive = 'mazayaExclusive'; static const mazayaExclusive = 'mazayaExclusive';
static const youAreAllCaughtUp = 'youAreAllCaughtUp'; static const youAreAllCaughtUp = 'youAreAllCaughtUp';
static const selectCity = 'selectCity'; static const selectCity = 'selectCity';
static const searchCities = 'searchCities';
} }

@ -13,6 +13,7 @@ import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_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'; import 'package:mohem_flutter_app/models/post_params_model.dart';
import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart';
@ -58,7 +59,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
bool _isLoadingExclusiveOffers = true; bool _isLoadingExclusiveOffers = true;
List<WalaPlusRetailerModel> _exclusiveOffers = <WalaPlusRetailerModel>[]; List<WalaPlusRetailerModel> _exclusiveOffers = <WalaPlusRetailerModel>[];
String selectedCity = "All Cities"; WalaPlusCityModel? selectedCity;
bool _isLoadingLatest = true; bool _isLoadingLatest = true;
List<WalaPlusRetailerModel> _latestRetailers = <WalaPlusRetailerModel>[]; List<WalaPlusRetailerModel> _latestRetailers = <WalaPlusRetailerModel>[];
@ -120,6 +121,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
categoryId: section.categoryId, categoryId: section.categoryId,
page: 1, page: 1,
perPage: 10, perPage: 10,
cityId: selectedCity?.cityId,
context: context, context: context,
); );
if (!mounted) return; if (!mounted) return;
@ -163,6 +165,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
sortBy: 'newest', sortBy: 'newest',
page: 1, page: 1,
perPage: 10, perPage: 10,
cityId: selectedCity?.cityId,
context: context, context: context,
); );
if (!mounted) return; if (!mounted) return;
@ -262,7 +265,12 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
AppRoutes.retailersListScreen, AppRoutes.retailersListScreen,
arguments: category, // Carries the currently selected city along so RetailersListScreen starts on it instead
// of defaulting back to "All Cities" - the user already chose a city on this screen.
arguments: <String, dynamic>{
if (category != null) 'category': category,
if (selectedCity != null) 'city': selectedCity,
},
); );
} }
@ -273,7 +281,10 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
AppRoutes.retailersListScreen, AppRoutes.retailersListScreen,
arguments: const <String, dynamic>{'exclusiveMode': true}, arguments: <String, dynamic>{
'exclusiveMode': true,
if (selectedCity != null) 'city': selectedCity,
},
); );
} }
@ -290,6 +301,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
name: LocaleKeys.allCategories.tr(), name: LocaleKeys.allCategories.tr(),
), ),
'searchMode': true, 'searchMode': true,
if (selectedCity != null) 'city': selectedCity,
}, },
); );
} }
@ -555,6 +567,7 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
} }
Widget _buildCategoriesSection() { Widget _buildCategoriesSection() {
bool isAr = AppState().isArabic(context);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 22), padding: const EdgeInsets.symmetric(horizontal: 22),
child: Column( child: Column(
@ -572,9 +585,13 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
onTap: onTap:
() => showMazayaCitySelectionBottomSheet( () => showMazayaCitySelectionBottomSheet(
context, context,
selectedCity: selectedCity, selectedCityId: selectedCity?.cityId,
onCitySelected: onCitySelected: (city) {
(city) => setState(() => selectedCity = city), setState(() => selectedCity = city);
_fetchLatestOnMazaya();
_fetchCategorySection(_travelSection);
_fetchCategorySection(_homeKitchenSection);
},
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -589,7 +606,9 @@ class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
), ),
), ),
const SizedBox(width: 3), const SizedBox(width: 3),
selectedCity.toText12(color: const Color(0xFF147D6B)), (selectedCity?.getDisplayName(isAr) ??
LocaleKeys.allCitiesOption.tr())
.toText12(color: const Color(0xFF147D6B)),
const SizedBox(width: 3), const SizedBox(width: 3),
SvgPicture.asset( SvgPicture.asset(
"assets/icons/mazaya_chevron_down.svg", "assets/icons/mazaya_chevron_down.svg",

@ -17,6 +17,7 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_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'; import 'package:mohem_flutter_app/models/post_params_model.dart';
import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart'; import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart';
@ -42,7 +43,7 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
WalaPlusCategoryModel? selectedCategory; WalaPlusCategoryModel? selectedCategory;
String currentCategoryId = ""; String currentCategoryId = "";
String selectedTypeFilter = "all"; String selectedTypeFilter = "all";
String selectedCity = "All Cities"; WalaPlusCityModel? selectedCity;
bool _isInitRouteArgs = false; bool _isInitRouteArgs = false;
// Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts // Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts
@ -125,6 +126,11 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
if (mounted) _searchFocusNode.requestFocus(); if (mounted) _searchFocusNode.requestFocus();
}); });
} }
// Preserves whichever city was already selected on Discover - starts here instead of
// resetting back to "All Cities" until the user explicitly changes it on this page.
if (args is Map && args['city'] is WalaPlusCityModel) {
selectedCity = args['city'] as WalaPlusCityModel;
}
} }
} }
@ -513,6 +519,7 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
sortBy: _filterSortBy, sortBy: _filterSortBy,
radius: radiusParam, radius: radiusParam,
minDiscount: minDiscountParam, minDiscount: minDiscountParam,
cityId: selectedCity?.cityId,
context: wantsNearest ? context : null, context: wantsNearest ? context : null,
userReference: null, userReference: null,
); );
@ -581,6 +588,7 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
sortBy: _filterSortBy, sortBy: _filterSortBy,
radius: radiusParam, radius: radiusParam,
minDiscount: minDiscountParam, minDiscount: minDiscountParam,
cityId: selectedCity?.cityId,
context: wantsNearest ? context : null, context: wantsNearest ? context : null,
userReference: null, userReference: null,
); );
@ -1063,10 +1071,9 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
color: Color(0xFF157D6B), color: Color(0xFF157D6B),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
selectedCity.toText13( (selectedCity?.getDisplayName(isAr) ??
isBold: true, LocaleKeys.allCitiesOption.tr())
color: const Color(0xFF157D6B), .toText13(isBold: true, color: const Color(0xFF157D6B)),
),
], ],
), ),
), ),
@ -1485,8 +1492,19 @@ class _RetailersListScreenState extends State<RetailersListScreen> {
void _showCitySelectionBottomSheet() { void _showCitySelectionBottomSheet() {
showMazayaCitySelectionBottomSheet( showMazayaCitySelectionBottomSheet(
context, context,
selectedCity: selectedCity, selectedCityId: selectedCity?.cityId,
onCitySelected: (city) => setState(() => selectedCity = city), onCitySelected: (city) {
setState(() => selectedCity = city);
String effectiveCategoryId =
_selectedSubcategoryId ??
(currentCategoryId.isEmpty
? (selectedCategory?.id ?? "")
: currentCategoryId);
fetchOffers(
categoryId: effectiveCategoryId,
query: _searchController.text,
);
},
); );
} }

@ -1,105 +1,196 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/extensions/string_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/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:shimmer/shimmer.dart';
// Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay // Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay
// visually/behaviorally identical - change the list or the sheet design here only. // visually/behaviorally identical - change the sheet design here only. Fetched fresh from
List<String> mazayaCitiesList(BuildContext context) { // getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan.
bool isAr = AppState().isArabic(context); void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged<WalaPlusCityModel> onCitySelected}) {
return <String>[
LocaleKeys.allCitiesOption.tr(),
LocaleKeys.riyadh.tr(),
isAr ? "جدة" : "Jeddah",
isAr ? "الدمام" : "Dammam",
isAr ? "الخبر" : "Khobar",
isAr ? "مكة المكرمة" : "Mecca",
isAr ? "المدينة المنورة" : "Medina",
];
}
void showMazayaCitySelectionBottomSheet(
BuildContext context, {
required String selectedCity,
required ValueChanged<String> onCitySelected,
}) {
List<String> cities = mazayaCitiesList(context);
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (BuildContext sheetContext) { builder: (BuildContext sheetContext) {
return Container( return _MazayaCitySelectionSheet(selectedCityId: selectedCityId, onCitySelected: onCitySelected);
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: () {
onCitySelected(city);
Navigator.pop(sheetContext);
},
);
}),
],
),
),
);
}, },
); );
} }
class _MazayaCitySelectionSheet extends StatefulWidget {
final int? selectedCityId;
final ValueChanged<WalaPlusCityModel> onCitySelected;
const _MazayaCitySelectionSheet({required this.selectedCityId, required this.onCitySelected});
@override
State<_MazayaCitySelectionSheet> createState() => _MazayaCitySelectionSheetState();
}
class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> {
bool _isLoading = true;
List<WalaPlusCityModel> _cities = <WalaPlusCityModel>[];
List<WalaPlusCityModel> _filteredCities = <WalaPlusCityModel>[];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_fetchCities();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchCities() async {
try {
List<WalaPlusCityModel> cities = await OffersAndDiscountsApiClient().getWalaPlusCities();
// "All Cities" always first - it's the default selection when nothing else was picked yet.
cities.sort((a, b) {
if (a.isAllCities == b.isAllCities) return 0;
return a.isAllCities ? -1 : 1;
});
if (mounted) {
setState(() {
_cities = cities;
_filteredCities = cities;
_isLoading = false;
});
}
} catch (e) {
debugPrint("MazayaCitySelector: error fetching cities: $e");
if (mounted) setState(() => _isLoading = false);
}
}
// Local/client-side only - the already-fetched list is small enough that there's no need to
// re-hit getWalaPlusCities per keystroke.
void _onSearchChanged(String query) {
String q = query.trim().toLowerCase();
setState(() {
_filteredCities =
q.isEmpty
? _cities
: _cities.where((WalaPlusCityModel c) {
return (c.cityNameEn ?? '').toLowerCase().contains(q) || (c.cityNameAr ?? '').toLowerCase().contains(q);
}).toList();
});
}
@override
Widget build(BuildContext context) {
bool isAr = AppState().isArabic(context);
// Fixed height regardless of result count - a search that narrows the list down to one or
// two cities (or the full unfiltered list) should never make the sheet itself grow/shrink,
// only the list inside it scrolls.
return Container(
height: MediaQuery.of(context).size.height * 0.75,
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(
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(context),
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),
_buildSearchField(),
const SizedBox(height: 12),
Expanded(child: _isLoading ? _buildShimmer() : _buildList(isAr)),
],
),
),
);
}
// Same design as MazayaDiscoverScreen's own search field - filters the already-fetched city
// list locally as the user types (see _onSearchChanged).
Widget _buildSearchField() {
return Container(
height: 47,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFE8EAE4)), borderRadius: BorderRadius.circular(12)),
child: Row(
children: <Widget>[
SvgPicture.asset("assets/icons/mazaya_search.svg", width: 18, height: 18, colorFilter: const ColorFilter.mode(Color(0xFF78817F), BlendMode.srcIn)),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: _searchController,
onChanged: _onSearchChanged,
decoration: InputDecoration(border: InputBorder.none, isDense: true, hintText: LocaleKeys.searchCities.tr(), hintStyle: const TextStyle(color: Color(0xFF78817F), fontSize: 12)),
style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)),
),
),
],
),
);
}
// Matches ListTile's own default one-line height (56) and contentPadding (16 horizontal) so
// there's no layout jump between the shimmer and the real list underneath it.
static const double _rowHeight = 56;
Widget _buildShimmer() {
return Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder:
(BuildContext _, int __) => Container(
height: _rowHeight,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: AlignmentDirectional.centerStart,
child: Container(height: 28, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8))),
),
),
);
}
Widget _buildList(bool isAr) {
if (_filteredCities.isEmpty) {
return Center(child: LocaleKeys.noResultsFound.tr().toText13(color: const Color(0xFF7A8180)));
}
// ListTile paints its background/ink splashes on the nearest Material ancestor - without
// this, the opaque DecoratedBox the whole sheet sits in (see build()) hides them.
return Material(
type: MaterialType.transparency,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _filteredCities.length,
itemBuilder: (BuildContext _, int index) {
WalaPlusCityModel city = _filteredCities[index];
bool isSelected = widget.selectedCityId != null ? city.cityId == widget.selectedCityId : city.isAllCities;
return ListTile(
title: city.getDisplayName(isAr).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: () {
widget.onCitySelected(city);
Navigator.pop(context);
},
);
},
),
);
}
}

Loading…
Cancel
Save