From 06295821aa23d3e5edcbc371a47a65b608aef2b2 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Mon, 26 Oct 2026 11:03:17 +0300 Subject: [PATCH] made the city search functional --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/api/offers_and_discounts_api_client.dart | 58 ++++ lib/classes/consts.dart | 65 +++-- lib/generated/codegen_loader.g.dart | 2 + lib/generated/locale_keys.g.dart | 1 + lib/ui/mazaya/mazaya_discover_screen.dart | 33 ++- lib/ui/mazaya/retailers_list_screen.dart | 32 +- .../mazaya/widgets/mazaya_city_selector.dart | 273 ++++++++++++------ 9 files changed, 341 insertions(+), 129 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index cbb7f31b..b0f59bc6 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -873,5 +873,6 @@ "offersCount": "{} عروض", "mazayaExclusive": "حصري لمزايا", "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", - "selectCity": "اختر المدينة" + "selectCity": "اختر المدينة", + "searchCities": "البحث عن المدن" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index fd4e1ad9..7b5619e9 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -877,6 +877,7 @@ "offersCount": "{} offers", "mazayaExclusive": "MAZAYA EXCLUSIVE", "youAreAllCaughtUp": "You're all caught up. More little extras soon.", - "selectCity": "Select City" + "selectCity": "Select City", + "searchCities": "Search cities" } diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index a5284a38..5c1eaab8 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -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_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_city_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; @@ -288,6 +289,58 @@ class OffersAndDiscountsApiClient { 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> getWalaPlusCities({ + String? mobileNumber, + String? country, + }) async { + String url = ApiConsts.walaPlusCities; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + + Map 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 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) { + cities.add(WalaPlusCityModel.fromJson(v)); + } else if (v is Map) { + cities.add( + WalaPlusCityModel.fromJson(Map.from(v)), + ); + } + } + } + } + return cities; + }, + url, + postParams, + ); + } + // Confirmed with backend team: WP_Offer_Type accepts "online" and "store". static const String walaPlusOfferTypeOnline = "online"; static const String walaPlusOfferTypeInStore = "store"; @@ -320,6 +373,10 @@ class OffersAndDiscountsApiClient { String? sortBy, int? radius, 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, String? mobileNumber, String? query, @@ -361,6 +418,7 @@ class OffersAndDiscountsApiClient { // here) must be sent as a concrete 0, not omitted. postParams["WP_Radius"] = radius ?? 0; postParams["WP_Min_Discount"] = minDiscount ?? 0; + postParams["WP_City_ID"] = cityId ?? 0; if (geoLocation != null && geoLocation.isNotEmpty) { postParams["WP_Geo_Location"] = geoLocation; diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index f07b470f..3c64cbb0 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -4,12 +4,12 @@ class ApiConsts { // 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://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 = "https://webservices.hmg.com"; // PreProd // 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 baseUrl = "https://uat.hmgwebservices.com"; // @@ -17,8 +17,10 @@ class ApiConsts { // static String baseUrl = "http://10.20.200.111:1010/"; // 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/api/"; // Elearning server live - static String enSATLiveURL = "https://ensat.hmg.com/auth/autologin"; // ENSAT server live + static String eLearningBaseUrl = + "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/"; @@ -29,7 +31,8 @@ class ApiConsts { static String baseUrlServices = baseUrl + "/Services/"; // server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server 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 swpRest = baseUrlServices + "SWP.svc/REST/"; static String user = baseUrlServices + "api/User/"; @@ -47,7 +50,9 @@ class ApiConsts { static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem"; static String walaPlusFavoritesAdd = erpRest + "WalaPlus_FavoritesAdd"; 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"; @@ -58,16 +63,24 @@ class ApiConsts { static String chatHubConnectionUrl = chatServerBaseUrl + "ConnectionChatHub"; //Groups - static String getGroupByUserId = chatServerBaseApiUrl + "group/getgroupsbyuserid/"; - static String deleteGroup = chatServerBaseApiUrl + "group/updateGroupIsDeleted/"; - static String updateGroupAdmin = chatServerBaseApiUrl + "group/updateGroupAdmin/"; - static String getGroupChatHistoryAsync = chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/"; - static String addGroupsAndUsers = chatServerBaseApiUrl + "group/addgroupandusers/"; - static String updateGroupsAndUsers = chatServerBaseApiUrl + "group/updategroupandusers/"; + static String getGroupByUserId = + chatServerBaseApiUrl + "group/getgroupsbyuserid/"; + static String deleteGroup = + chatServerBaseApiUrl + "group/updateGroupIsDeleted/"; + static String updateGroupAdmin = + 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 chatRecentUrl = chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem - static String chatSingleUserHistoryUrl = chatServerBaseApiUrl + "UserChatHistory/"; + static String chatRecentUrl = + chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem + static String chatSingleUserHistoryUrl = + chatServerBaseApiUrl + "UserChatHistory/"; static String chatMediaImageUploadUrl = chatServerBaseApiUrl + "shared/"; static String chatFavUser = chatServerBaseApiUrl + "FavUser/"; static String chatUserImages = chatServerBaseUrl + "empservice/api/employee/"; @@ -80,25 +93,33 @@ class ApiConsts { // static String marathonBaseUrl = marathonBaseUrlUAT; 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 marathonUpcomingUrl = marathonBaseUrl + "marathon/upcoming/"; - static String marathonJoinParticipantUrl = marathonBaseUrl + "participant/join"; + static String marathonJoinParticipantUrl = + marathonBaseUrl + "participant/join"; static String marathonNextQuestionUrl = marathonBaseUrl + "question/next"; static String marathonSubmitAnswerUrl = marathonBaseUrl + "question/submit"; static String marathonQualifiersUrl = marathonBaseUrl + "winner/getWinner/"; - static String marathonSelectedWinner = marathonBaseUrl + "winner/getSelectedWinner/"; - static String marathonGetMarathonersCount = marathonBaseUrl + "Participant/GetRemainingParticipants"; - static String marathonGetTutorial = marathonBaseUrl + "tutorial/GetTutorialNotification"; + static String marathonSelectedWinner = + marathonBaseUrl + "winner/getSelectedWinner/"; + static String marathonGetMarathonersCount = + marathonBaseUrl + "Participant/GetRemainingParticipants"; + static String marathonGetTutorial = + marathonBaseUrl + "tutorial/GetTutorialNotification"; //DummyCards for the UI static int tabletMinLength = 500; // Notification Center GUIDs for OTP - static String notificationCenterAppGUID = "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0"; - static String notificationCenterLoginServiceGUID = "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2"; - static String notificationCenterForgotPasswordServiceGUID = "15AA0C70-8DA3-430B-8881-EAFBB508F296"; + static String notificationCenterAppGUID = + "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0"; + static String notificationCenterLoginServiceGUID = + "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2"; + static String notificationCenterForgotPasswordServiceGUID = + "15AA0C70-8DA3-430B-8881-EAFBB508F296"; static MemberType selectedMemberType = MemberType.employee; diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 23a087c2..daaca8e6 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -924,6 +924,7 @@ class CodegenLoader extends AssetLoader { "mazayaExclusive": "حصري لمزايا", "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", "selectCity": "اختر المدينة", + "searchCities": "البحث عن المدن", }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1848,6 +1849,7 @@ class CodegenLoader extends AssetLoader { "mazayaExclusive": "MAZAYA EXCLUSIVE", "youAreAllCaughtUp": "You're all caught up. More little extras soon.", "selectCity": "Select City", + "searchCities": "Search cities", }; static const Map> mapLocales = { "ar_SA": _ar_SA, diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 93b787f2..e700292b 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -888,4 +888,5 @@ abstract class LocaleKeys { static const mazayaExclusive = 'mazayaExclusive'; static const youAreAllCaughtUp = 'youAreAllCaughtUp'; static const selectCity = 'selectCity'; + static const searchCities = 'searchCities'; } diff --git a/lib/ui/mazaya/mazaya_discover_screen.dart b/lib/ui/mazaya/mazaya_discover_screen.dart index 56d8627a..cfb9cfe7 100644 --- a/lib/ui/mazaya/mazaya_discover_screen.dart +++ b/lib/ui/mazaya/mazaya_discover_screen.dart @@ -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/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_city_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/ui/mazaya/widgets/mazaya_bottom_nav.dart'; @@ -58,7 +59,7 @@ class _MazayaDiscoverScreenState extends State { bool _isLoadingExclusiveOffers = true; List _exclusiveOffers = []; - String selectedCity = "All Cities"; + WalaPlusCityModel? selectedCity; bool _isLoadingLatest = true; List _latestRetailers = []; @@ -120,6 +121,7 @@ class _MazayaDiscoverScreenState extends State { categoryId: section.categoryId, page: 1, perPage: 10, + cityId: selectedCity?.cityId, context: context, ); if (!mounted) return; @@ -163,6 +165,7 @@ class _MazayaDiscoverScreenState extends State { sortBy: 'newest', page: 1, perPage: 10, + cityId: selectedCity?.cityId, context: context, ); if (!mounted) return; @@ -262,7 +265,12 @@ class _MazayaDiscoverScreenState extends State { Navigator.pushNamed( context, 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: { + if (category != null) 'category': category, + if (selectedCity != null) 'city': selectedCity, + }, ); } @@ -273,7 +281,10 @@ class _MazayaDiscoverScreenState extends State { Navigator.pushNamed( context, AppRoutes.retailersListScreen, - arguments: const {'exclusiveMode': true}, + arguments: { + 'exclusiveMode': true, + if (selectedCity != null) 'city': selectedCity, + }, ); } @@ -290,6 +301,7 @@ class _MazayaDiscoverScreenState extends State { name: LocaleKeys.allCategories.tr(), ), 'searchMode': true, + if (selectedCity != null) 'city': selectedCity, }, ); } @@ -555,6 +567,7 @@ class _MazayaDiscoverScreenState extends State { } Widget _buildCategoriesSection() { + bool isAr = AppState().isArabic(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Column( @@ -572,9 +585,13 @@ class _MazayaDiscoverScreenState extends State { onTap: () => showMazayaCitySelectionBottomSheet( context, - selectedCity: selectedCity, - onCitySelected: - (city) => setState(() => selectedCity = city), + selectedCityId: selectedCity?.cityId, + onCitySelected: (city) { + setState(() => selectedCity = city); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + }, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -589,7 +606,9 @@ class _MazayaDiscoverScreenState extends State { ), ), 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), SvgPicture.asset( "assets/icons/mazaya_chevron_down.svg", diff --git a/lib/ui/mazaya/retailers_list_screen.dart b/lib/ui/mazaya/retailers_list_screen.dart index dd4fd045..8860c24f 100644 --- a/lib/ui/mazaya/retailers_list_screen.dart +++ b/lib/ui/mazaya/retailers_list_screen.dart @@ -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/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_city_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/ui/mazaya/widgets/mazaya_city_selector.dart'; @@ -42,7 +43,7 @@ class _RetailersListScreenState extends State { WalaPlusCategoryModel? selectedCategory; String currentCategoryId = ""; String selectedTypeFilter = "all"; - String selectedCity = "All Cities"; + WalaPlusCityModel? selectedCity; bool _isInitRouteArgs = false; // Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts @@ -125,6 +126,11 @@ class _RetailersListScreenState extends State { 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 { sortBy: _filterSortBy, radius: radiusParam, minDiscount: minDiscountParam, + cityId: selectedCity?.cityId, context: wantsNearest ? context : null, userReference: null, ); @@ -581,6 +588,7 @@ class _RetailersListScreenState extends State { sortBy: _filterSortBy, radius: radiusParam, minDiscount: minDiscountParam, + cityId: selectedCity?.cityId, context: wantsNearest ? context : null, userReference: null, ); @@ -1063,10 +1071,9 @@ class _RetailersListScreenState extends State { color: Color(0xFF157D6B), ), const SizedBox(width: 4), - selectedCity.toText13( - isBold: true, - color: const Color(0xFF157D6B), - ), + (selectedCity?.getDisplayName(isAr) ?? + LocaleKeys.allCitiesOption.tr()) + .toText13(isBold: true, color: const Color(0xFF157D6B)), ], ), ), @@ -1485,8 +1492,19 @@ class _RetailersListScreenState extends State { void _showCitySelectionBottomSheet() { showMazayaCitySelectionBottomSheet( context, - selectedCity: selectedCity, - onCitySelected: (city) => setState(() => selectedCity = city), + selectedCityId: selectedCity?.cityId, + onCitySelected: (city) { + setState(() => selectedCity = city); + String effectiveCategoryId = + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); + fetchOffers( + categoryId: effectiveCategoryId, + query: _searchController.text, + ); + }, ); } diff --git a/lib/ui/mazaya/widgets/mazaya_city_selector.dart b/lib/ui/mazaya/widgets/mazaya_city_selector.dart index 80b53a8c..41fff782 100644 --- a/lib/ui/mazaya/widgets/mazaya_city_selector.dart +++ b/lib/ui/mazaya/widgets/mazaya_city_selector.dart @@ -1,105 +1,196 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.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 -// visually/behaviorally identical - change the list or the sheet design here only. -List mazayaCitiesList(BuildContext context) { - bool isAr = AppState().isArabic(context); - return [ - 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 onCitySelected, -}) { - List cities = mazayaCitiesList(context); - +// visually/behaviorally identical - change the sheet design here only. Fetched fresh from +// getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan. +void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged onCitySelected}) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, + isScrollControlled: true, 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: () { - onCitySelected(city); - Navigator.pop(sheetContext); - }, - ); - }), - ], - ), - ), - ); + return _MazayaCitySelectionSheet(selectedCityId: selectedCityId, onCitySelected: onCitySelected); }, ); } + +class _MazayaCitySelectionSheet extends StatefulWidget { + final int? selectedCityId; + final ValueChanged onCitySelected; + + const _MazayaCitySelectionSheet({required this.selectedCityId, required this.onCitySelected}); + + @override + State<_MazayaCitySelectionSheet> createState() => _MazayaCitySelectionSheetState(); +} + +class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> { + bool _isLoading = true; + List _cities = []; + List _filteredCities = []; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _fetchCities(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _fetchCities() async { + try { + List 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: [ + 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); + }, + ); + }, + ), + ); + } +}