diff --git a/android/build.gradle b/android/build.gradle index 8a97f51..46c8995 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -66,6 +66,20 @@ allprojects { jvmTarget = "21" } } + + if (project.name == 'nfc_manager') { + project.tasks.matching { it.name.startsWith("compile") && it.name.endsWith("Kotlin") }.configureEach { + doFirst { + def translatorFile = project.file("src/main/kotlin/io/flutter/plugins/nfcmanager/Translator.kt") + if (translatorFile.exists()) { + def content = translatorFile.text + if (content.contains("toLowerCase(Locale.ROOT)")) { + translatorFile.text = content.replace("toLowerCase(Locale.ROOT)", "lowercase(Locale.ROOT)") + } + } + } + } + } } } } diff --git a/assets/images/all_categories.png b/assets/images/all_categories.png deleted file mode 100644 index aa6f895..0000000 Binary files a/assets/images/all_categories.png and /dev/null differ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 4861521..973a5b0 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -782,6 +782,7 @@ "showAllLocations": "عرض جميع الفروع ({})", "searchBranches": "البحث عن الفروع أو المدن...", "allCities": "الكل", + "allCitiesOption": "جميع المدن", "noBranchesFound": "لم يتم العثور على فروع مطابقة", "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", "branchesCount": "{} فرع", @@ -829,6 +830,27 @@ "merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح", "locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك", "locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك", + "mazayaDeals": "عروض مزايا", + "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط", + "exploreYourBenefits": "استكشف مزاياك", + "theMazayaEdit": "اختيارات مزايا", + "exclusivelyYours": "حصرياً لك", + "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.", + "seeAll": "عرض الكل", + "latestOnMazaya": "أحدث ما في مزايا", + "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.", + "travelEssentials": "أساسيات السفر", + "fromEverydayCareToYourNextAdventure": "من العناية اليومية إلى مغامرتك القادمة.", + "homeAndKitchen": "المنزل والمطبخ", + "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.", + "discover": "اكتشف", + "saved": "المحفوظات", + "myFamily": "عائلتي", + "educationAndTraining": "التعليم والتدريب", + "foodsAndDrinks": "الأطعمة والمشروبات", + "travelAndEntertainment": "السفر والترفيه", + "beautyAndFitness": "الجمال واللياقة", + "healthcare": "الرعاية الصحية", "favoriteMerchant": "العروض المفضلة", "favoriteOffers": "العروض المفضلة", "goodToKnow": "معلومات مهمة", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 47bfca8..5a4bc2c 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -786,6 +786,7 @@ "showAllLocations": "Show all {} locations", "searchBranches": "Search branches or cities...", "allCities": "All", + "allCitiesOption": "All Cities", "noBranchesFound": "No branches found", "browseAllBranches": "Browse all {} branches across cities", "branchesCount": "{} Branches", @@ -830,6 +831,27 @@ "merchantWebsiteNotAvailable": "Merchant website not available", "locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers", "locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers", + "mazayaDeals": "Mazaya Deals", + "findABrandOrSomething": "Find a brand or a little something", + "exploreYourBenefits": "Explore your benefits", + "theMazayaEdit": "The Mazaya Edit", + "exclusivelyYours": "Exclusively yours", + "aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.", + "seeAll": "See all", + "latestOnMazaya": "Latest on Mazaya", + "goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.", + "travelEssentials": "Travel Essentials", + "fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.", + "homeAndKitchen": "Home & Kitchen", + "curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.", + "discover": "Discover", + "saved": "Saved", + "myFamily": "My Family", + "educationAndTraining": "Education & Training", + "foodsAndDrinks": "Foods & Drinks", + "travelAndEntertainment": "Travel & Entertainment", + "beautyAndFitness": "Beauty & Fitness", + "healthcare": "Healthcare", "favoriteAddedSuccess": "Added to favorites successfully", "favoriteRemovedSuccess": "Removed from favorites successfully", "favoriteError": "An error occurred while updating favorites", diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6aac74d..03a06b1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 76AE113C28E078EA00513112 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 76AE113B28E078EA00513112 /* GoogleService-Info.plist */; }; @@ -57,7 +57,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */, + 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -144,7 +144,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, C4CFBC4C5CAC00182015ACD5 /* [CP] Embed Pods Frameworks */, - EF29CE25BB57F5AA48CE1136 /* [CP] Copy Pods Resources */, + 8ED1493DD0011941609D6F99 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -242,7 +242,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6C6E76E4FB19FAF11C24005D /* [CP] Copy Pods Resources */ = { + 8ED1493DD0011941609D6F99 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -367,7 +367,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -454,7 +454,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -504,7 +504,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c12df5..bb7d20f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -3,9 +3,23 @@ LastUpgradeVersion = "1510" version = "1.3"> + + + + sms tel mailto + https + http + comgooglemaps + googlechromes + waze LSRequiresIPhoneOS @@ -46,19 +51,23 @@ NSCameraUsageDescription - This app requires camera access to capture & upload picture as profile image. + This app requires camera access to capture & upload picture as profile image, and scan QR codes. NSFaceIDUsageDescription This app requires Face ID to allow biometric authentication for app login. + NSLocalNetworkUsageDescription + This app requires access to local network to connect to company Wi-Fi networks for attendance. NSLocationAlwaysAndWhenInUseUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSLocationAlwaysUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSLocationWhenInUseUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSMicrophoneUsageDescription - This app requires microphone access to for call. + This app requires microphone access for audio calls and recording voice messages. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. + NSPhotoLibraryAddUsageDescription + This app requires access to save photos and documents to your photo library. UIApplicationSupportsIndirectInputEvents NSSpeechRecognitionUsageDescription diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 304aa00..98a55e2 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -20,6 +20,7 @@ com.apple.developer.nfc.readersession.formats + NDEF TAG com.apple.developer.ubiquity-container-identifiers diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart index 0a2a3d9..65e5fc6 100644 --- a/lib/api/login_api_client.dart +++ b/lib/api/login_api_client.dart @@ -294,9 +294,9 @@ class LoginApiClient { return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams); } - Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName) async { + Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName, String loginType) async { String url = "${ApiConsts.erpRest}CheckActivationCode"; - Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName}; + Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName, "LoginType":loginType}; postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index 1ad8144..e85b04b 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -258,6 +258,11 @@ class OffersAndDiscountsApiClient { Future getWalaPlusRetailers({ String? categoryId, + // Every existing caller passes categoryId (even "" for "all") and relies on WP_CategoryID + // always being present in the request - default true preserves that for everyone. Pass + // false only when the category filter must be fully omitted (e.g. a cross-category newest + // feed), not just set to empty. + bool includeCategoryId = true, int page = 1, int perPage = 10, String? geoLocation, @@ -285,7 +290,9 @@ class OffersAndDiscountsApiClient { } postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1; postParams["CompanyID"] = 1; - postParams["WP_CategoryID"] = effectiveCatId; + if (includeCategoryId) { + postParams["WP_CategoryID"] = effectiveCatId; + } if (offerType != null && offerType.trim().isNotEmpty) { postParams["WP_Offer_Type"] = offerType.trim(); } @@ -323,8 +330,6 @@ class OffersAndDiscountsApiClient { postParams["WP_User_Reference"] = null; } - log("postParams: ${postParams.toString()}"); - return await ApiClient().postJsonForObject( (response) { if (response != null) { diff --git a/lib/api/profile_api_client.dart b/lib/api/profile_api_client.dart index 23197a5..7d58478 100644 --- a/lib/api/profile_api_client.dart +++ b/lib/api/profile_api_client.dart @@ -551,6 +551,7 @@ class ProfileApiClient { if (personId != null) { postParams["P_PERSON_ID"] = personId; } + postParams["PatientIdentificationID"] = patientIdentificationId?.replaceAll('-', '').trim(); // Only the Grant call carries these - Revoke's payload stays exactly as it was before // this feature. P_BIRTH_DATE/P_NHIC_VERIFIED_FLAG are always sent (even as null, if the // source contact from GET_EMPLOYEE_CONTACTS didn't have a value) once we're granting. @@ -561,7 +562,7 @@ class ProfileApiClient { if (patientIdentificationId != null && patientIdentificationId.trim().isNotEmpty) { // National ID comes formatted with dashes (e.g. "2-5616-1545-7") - the backend wants // the plain digit string (e.g. "2561615457"). - postParams["PatientIdentificationID"] = patientIdentificationId.replaceAll('-', '').trim(); + } } postParams.addAll(AppState().postParamsJson); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index db46a5c..df6c76f 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -14,8 +14,6 @@ import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_create_request.dart'; import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_ovr_home.dart'; import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_request_detailed.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; -import 'package:mohem_flutter_app/ui/landing/dashboard_screen_for_dependet.dart'; -import 'package:mohem_flutter_app/ui/landing/wala_plus_retailer_details_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart'; import 'package:mohem_flutter_app/ui/landing/itg/survey_screen.dart'; @@ -33,10 +31,15 @@ import 'package:mohem_flutter_app/ui/marathon/marathon_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_sponsor_video_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_tutorial_viewer_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_waiting_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/mazaya_discover_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailer_details_screen.dart'; import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/my_team/create_request.dart'; import 'package:mohem_flutter_app/ui/my_team/employee_details.dart'; @@ -64,6 +67,7 @@ import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details. import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart'; import 'package:mohem_flutter_app/ui/screens/child_education/child_education_assistance.dart'; import 'package:mohem_flutter_app/ui/screens/courses/courses_screen.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart'; import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart'; import 'package:mohem_flutter_app/ui/screens/event_activity/event_activity.dart'; @@ -103,8 +107,10 @@ class AppRoutes { static const String newPassword = "/newPassword"; static const String loginVerification = "/loginVerification"; static const String dashboard = "/dashboard"; - static const String dashboardScreenForDependet = "/dashboardScreenForDependet"; - static const String walaPlusRetailerDetails = "/walaPlusRetailerDetails"; + static const String retailersListScreen = "/dashboardScreenForDependet"; + static const String retailerDetailsScreen = "/walaPlusRetailerDetails"; + static const String mazayaDiscover = "/mazayaDiscover"; + static const String savedReyailersScreen = "/mazayaSaved"; static const String todayAttendance = "/todayAttendance"; static const String subMenuScreen = "/submenuScreen"; static const String addEitScreen = "/addeitScreen"; @@ -243,15 +249,14 @@ class AppRoutes { verifyLogin: (BuildContext context) => VerifyLoginScreen(), verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(), dashboard: (BuildContext context) => DashboardScreen(), - dashboardScreenForDependet: (BuildContext context) => const DashboardScreenForDependet(), - walaPlusRetailerDetails: (BuildContext context) => const WalaPlusRetailerDetailsScreen(), + retailersListScreen: (BuildContext context) => const RetailersListScreen(), + retailerDetailsScreen: (BuildContext context) => const RetailerDetailsScreen(), + mazayaDiscover: (BuildContext context) => const MazayaDiscoverScreen(), + savedReyailersScreen: (BuildContext context) => const SavedRetailersScreen(), appInAppWebView: (BuildContext context) { Object? args = ModalRoute.of(context)?.settings.arguments; if (args is Map) { - return AppInAppWebViewScreen( - url: args['url'] ?? "", - title: args['title'], - ); + return AppInAppWebViewScreen(url: args['url'] ?? "", title: args['title']); } return const Scaffold(); }, @@ -262,8 +267,8 @@ class AppRoutes { newPassword: (BuildContext context) => NewPasswordScreen(), forgotPassword: (BuildContext context) => ForgotPasswordScreen(), todayAttendance: (BuildContext context) => TodayAttendanceScreen2(), - //eit + //eit addEitScreen: (BuildContext context) => AddEITScreen(), //Work List @@ -274,7 +279,6 @@ class AppRoutes { worklistSettings: (BuildContext context) => WorklistSettings(), // Leave Balance - leaveBalance: (BuildContext context) => LeaveBalance(), addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(), @@ -373,7 +377,7 @@ class AppRoutes { unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(), appUpdateScreen: (BuildContext context) => const AppUpdateScreen(), childEducation: (BuildContext context) => ChildEducationAssistance(), - activityScreen:(BuildContext context) => const EventActivityScreen(), + activityScreen: (BuildContext context) => const EventActivityScreen(), //ETQAN OVR etqanOvr: (BuildContext context) => EtqanOvrHome(), @@ -382,6 +386,5 @@ class AppRoutes { //Parking QR parkingQr: (BuildContext context) => const ParkingQrScreen(), - }; } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 6b8457e..42fba96 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -798,6 +798,7 @@ class CodegenLoader extends AssetLoader{ "showAllLocations": "عرض جميع الفروع ({})", "searchBranches": "البحث عن الفروع أو المدن...", "allCities": "الكل", + "allCitiesOption": "جميع المدن", "noBranchesFound": "لم يتم العثور على فروع مطابقة", "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", "branchesCount": "{} فرع", @@ -845,6 +846,27 @@ class CodegenLoader extends AssetLoader{ "merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح", "locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك", "locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك", + "mazayaDeals": "عروض مزايا", + "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط", + "exploreYourBenefits": "استكشف مزاياك", + "theMazayaEdit": "اختيارات مزايا", + "exclusivelyYours": "حصرياً لك", + "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.", + "seeAll": "عرض الكل", + "latestOnMazaya": "أحدث ما في مزايا", + "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.", + "travelEssentials": "أساسيات السفر", + "fromEverydayCareToYourNextAdventure": "من العناية اليومية إلى مغامرتك القادمة.", + "homeAndKitchen": "المنزل والمطبخ", + "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.", + "discover": "اكتشف", + "saved": "المحفوظات", + "myFamily": "عائلتي", + "educationAndTraining": "التعليم والتدريب", + "foodsAndDrinks": "الأطعمة والمشروبات", + "travelAndEntertainment": "السفر والترفيه", + "beautyAndFitness": "الجمال واللياقة", + "healthcare": "الرعاية الصحية", "favoriteMerchant": "العروض المفضلة", "favoriteOffers": "العروض المفضلة", "goodToKnow": "معلومات مهمة", @@ -858,15 +880,13 @@ class CodegenLoader extends AssetLoader{ "phone": "الهاتف", "viewAll": "عرض الكل", "moreCitiesCount": "+ {} مدن أخرى", - "mazayaDeals": "عروض مزايا", - "exploreYourBenefits": "استكشف مزاياك", "findAnEscapeOrAnExperience": "ابحث عن تجربة أو عطلة", "allOffers": "جميع العروض", "sampleAvailability": "حسب التوفر", "offersCount": "{} عروض", "mazayaExclusive": "حصري لمزايا", "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", - "selectCity": "اختر المدينة" + "selectCity": "اختر المدينة", }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1652,6 +1672,7 @@ static const Map _en_US = { "showAllLocations": "Show all {} locations", "searchBranches": "Search branches or cities...", "allCities": "All", + "allCitiesOption": "All Cities", "noBranchesFound": "No branches found", "browseAllBranches": "Browse all {} branches across cities", "branchesCount": "{} Branches", @@ -1696,6 +1717,27 @@ static const Map _en_US = { "merchantWebsiteNotAvailable": "Merchant website not available", "locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers", "locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers", + "mazayaDeals": "Mazaya Deals", + "findABrandOrSomething": "Find a brand or a little something", + "exploreYourBenefits": "Explore your benefits", + "theMazayaEdit": "The Mazaya Edit", + "exclusivelyYours": "Exclusively yours", + "aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.", + "seeAll": "See all", + "latestOnMazaya": "Latest on Mazaya", + "goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.", + "travelEssentials": "Travel Essentials", + "fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.", + "homeAndKitchen": "Home & Kitchen", + "curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.", + "discover": "Discover", + "saved": "Saved", + "myFamily": "My Family", + "educationAndTraining": "Education & Training", + "foodsAndDrinks": "Foods & Drinks", + "travelAndEntertainment": "Travel & Entertainment", + "beautyAndFitness": "Beauty & Fitness", + "healthcare": "Healthcare", "favoriteAddedSuccess": "Added to favorites successfully", "favoriteRemovedSuccess": "Removed from favorites successfully", "favoriteError": "An error occurred while updating favorites", @@ -1712,15 +1754,13 @@ static const Map _en_US = { "phone": "Phone", "viewAll": "View all", "moreCitiesCount": "+ {} more cities", - "mazayaDeals": "Mazaya Deals", - "exploreYourBenefits": "Explore your benefits", "findAnEscapeOrAnExperience": "Find an escape or an experience", "allOffers": "All offers", "sampleAvailability": "sample availability", "offersCount": "{} offers", "mazayaExclusive": "MAZAYA EXCLUSIVE", "youAreAllCaughtUp": "You're all caught up. More little extras soon.", - "selectCity": "Select City" + "selectCity": "Select City", }; 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 f1ee6c4..9bc9bfd 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -770,6 +770,7 @@ abstract class LocaleKeys { static const showAllLocations = 'showAllLocations'; static const searchBranches = 'searchBranches'; static const allCities = 'allCities'; + static const allCitiesOption = 'allCitiesOption'; static const noBranchesFound = 'noBranchesFound'; static const browseAllBranches = 'browseAllBranches'; static const branchesCount = 'branchesCount'; @@ -817,6 +818,27 @@ abstract class LocaleKeys { static const merchantWebsiteNotAvailable = 'merchantWebsiteNotAvailable'; static const locationServicesRequiredForNearestOffers = 'locationServicesRequiredForNearestOffers'; static const locationPermissionRequiredForNearestOffers = 'locationPermissionRequiredForNearestOffers'; + static const mazayaDeals = 'mazayaDeals'; + static const findABrandOrSomething = 'findABrandOrSomething'; + static const exploreYourBenefits = 'exploreYourBenefits'; + static const theMazayaEdit = 'theMazayaEdit'; + static const exclusivelyYours = 'exclusivelyYours'; + static const aLittleExtraJustForMazayaMembers = 'aLittleExtraJustForMazayaMembers'; + static const seeAll = 'seeAll'; + static const latestOnMazaya = 'latestOnMazaya'; + static const goodFoodGreatCompanyEvenBetterPerks = 'goodFoodGreatCompanyEvenBetterPerks'; + static const travelEssentials = 'travelEssentials'; + static const fromEverydayCareToYourNextAdventure = 'fromEverydayCareToYourNextAdventure'; + static const homeAndKitchen = 'homeAndKitchen'; + static const curatedPicksForYourHomeAndKitchen = 'curatedPicksForYourHomeAndKitchen'; + static const discover = 'discover'; + static const saved = 'saved'; + static const myFamily = 'myFamily'; + static const educationAndTraining = 'educationAndTraining'; + static const foodsAndDrinks = 'foodsAndDrinks'; + static const travelAndEntertainment = 'travelAndEntertainment'; + static const beautyAndFitness = 'beautyAndFitness'; + static const healthcare = 'healthcare'; static const favoriteMerchant = 'favoriteMerchant'; static const favoriteOffers = 'favoriteOffers'; static const goodToKnow = 'goodToKnow'; @@ -830,8 +852,6 @@ abstract class LocaleKeys { static const phone = 'phone'; static const viewAll = 'viewAll'; static const moreCitiesCount = 'moreCitiesCount'; - static const mazayaDeals = 'mazayaDeals'; - static const exploreYourBenefits = 'exploreYourBenefits'; static const findAnEscapeOrAnExperience = 'findAnEscapeOrAnExperience'; static const allOffers = 'allOffers'; static const sampleAvailability = 'sampleAvailability'; diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart index 054d741..c124e6b 100644 --- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:easy_localization/easy_localization.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'; class WalaPlusRetailersResponse { @@ -208,6 +209,11 @@ class WalaPlusRetailerModel { String? countryNameEn; bool? isFavorite; + // Drives the "MAZAYA EXCLUSIVE" badge on the retailer card (Figma node 927:496). NOT parsed + // from the WalaPlus_Retailers/WalaPlus_RetailersByID API - this is a different, separate + // "Exclusive" API/section, so callers set this manually on the model after fetching from there. + bool? isExclusive; + WalaPlusRetailerModel({ this.id, this.name, @@ -221,6 +227,7 @@ class WalaPlusRetailerModel { this.logoThumb, this.website, this.isFavorite, + this.isExclusive, this.type, this.usage, this.weight, @@ -338,38 +345,44 @@ class WalaPlusRetailerModel { categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim())); } - String? singleCatName = (targetJson['category_name'] ?? - targetJson['categoryName'] ?? - targetJson['cat_name'] ?? - targetJson['catName'] ?? - targetJson['CategoryName'] ?? - targetJson['Category_Name'] ?? - targetJson['category_title'] ?? - targetJson['categoryTitle'])?.toString().trim(); - String? singleCatAr = (targetJson['category_name_ar'] ?? - targetJson['categoryNameAr'] ?? - targetJson['cat_name_ar'] ?? - targetJson['catNameAr'] ?? - targetJson['category_ar'] ?? - targetJson['country_name_ar'] ?? - targetJson['countryNameAr'])?.toString().trim(); - String? singleCatEn = (targetJson['category_name_en'] ?? - targetJson['categoryNameEn'] ?? - targetJson['cat_name_en'] ?? - targetJson['catNameEn'] ?? - targetJson['category_en'] ?? - targetJson['country_name_en'] ?? - targetJson['countryNameEn'])?.toString().trim(); - String? foundCategoryId = (targetJson['category_id'] ?? - targetJson['categoryId'] ?? - targetJson['cat_id'] ?? - targetJson['catId'] ?? - targetJson['WP_CategoryID'] ?? - targetJson['WP_CategoryId'])?.toString().trim(); + String? singleCatName = + (targetJson['category_name'] ?? + targetJson['categoryName'] ?? + targetJson['cat_name'] ?? + targetJson['catName'] ?? + targetJson['CategoryName'] ?? + targetJson['Category_Name'] ?? + targetJson['category_title'] ?? + targetJson['categoryTitle']) + ?.toString() + .trim(); + String? singleCatAr = + (targetJson['category_name_ar'] ?? + targetJson['categoryNameAr'] ?? + targetJson['cat_name_ar'] ?? + targetJson['catNameAr'] ?? + targetJson['category_ar'] ?? + targetJson['country_name_ar'] ?? + targetJson['countryNameAr']) + ?.toString() + .trim(); + String? singleCatEn = + (targetJson['category_name_en'] ?? + targetJson['categoryNameEn'] ?? + targetJson['cat_name_en'] ?? + targetJson['catNameEn'] ?? + targetJson['category_en'] ?? + targetJson['country_name_en'] ?? + targetJson['countryNameEn']) + ?.toString() + .trim(); + String? foundCategoryId = + (targetJson['category_id'] ?? targetJson['categoryId'] ?? targetJson['cat_id'] ?? targetJson['catId'] ?? targetJson['WP_CategoryID'] ?? targetJson['WP_CategoryId'])?.toString().trim(); String? rawType = targetJson['type']?.toString().trim(); String? lowerType = rawType?.toLowerCase(); - bool isTypeStoreKeyword = lowerType == null || + bool isTypeStoreKeyword = + lowerType == null || lowerType.isEmpty || lowerType == 'online' || lowerType == 'instore' || @@ -387,12 +400,9 @@ class WalaPlusRetailerModel { if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") || (singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") || (singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) { - categoriesList.add(WalaPlusCategoryModel( - id: foundCategoryId, - name: singleCatName ?? singleCatEn ?? singleCatAr, - countryNameAr: singleCatAr ?? singleCatName, - countryNameEn: singleCatEn ?? singleCatName, - )); + categoriesList.add( + WalaPlusCategoryModel(id: foundCategoryId, name: singleCatName ?? singleCatEn ?? singleCatAr, countryNameAr: singleCatAr ?? singleCatName, countryNameEn: singleCatEn ?? singleCatName), + ); } } @@ -429,7 +439,8 @@ class WalaPlusRetailerModel { } } - String? foundWebsite = targetJson['website']?.toString() ?? + String? foundWebsite = + targetJson['website']?.toString() ?? targetJson['merchant_website']?.toString() ?? targetJson['merchantWebsite']?.toString() ?? targetJson['website_url']?.toString() ?? @@ -464,25 +475,13 @@ class WalaPlusRetailerModel { bool? parsedIsFavorite; if (targetJson.containsKey('isfavorite')) { - parsedIsFavorite = targetJson['isfavorite'] == true || - targetJson['isfavorite'] == 1 || - targetJson['isfavorite'] == 'true' || - targetJson['isfavorite'] == '1'; + parsedIsFavorite = targetJson['isfavorite'] == true || targetJson['isfavorite'] == 1 || targetJson['isfavorite'] == 'true' || targetJson['isfavorite'] == '1'; } else if (targetJson.containsKey('is_favorite')) { - parsedIsFavorite = targetJson['is_favorite'] == true || - targetJson['is_favorite'] == 1 || - targetJson['is_favorite'] == 'true' || - targetJson['is_favorite'] == '1'; + parsedIsFavorite = targetJson['is_favorite'] == true || targetJson['is_favorite'] == 1 || targetJson['is_favorite'] == 'true' || targetJson['is_favorite'] == '1'; } else if (targetJson.containsKey('isFavorite')) { - parsedIsFavorite = targetJson['isFavorite'] == true || - targetJson['isFavorite'] == 1 || - targetJson['isFavorite'] == 'true' || - targetJson['isFavorite'] == '1'; + parsedIsFavorite = targetJson['isFavorite'] == true || targetJson['isFavorite'] == 1 || targetJson['isFavorite'] == 'true' || targetJson['isFavorite'] == '1'; } else if (targetJson.containsKey('favorite')) { - parsedIsFavorite = targetJson['favorite'] == true || - targetJson['favorite'] == 1 || - targetJson['favorite'] == 'true' || - targetJson['favorite'] == '1'; + parsedIsFavorite = targetJson['favorite'] == true || targetJson['favorite'] == 1 || targetJson['favorite'] == 'true' || targetJson['favorite'] == '1'; } return WalaPlusRetailerModel( @@ -532,6 +531,54 @@ class WalaPlusRetailerModel { ); } + // Adapter for the Mazaya "Exclusive Offers" section, which reuses the legacy Offers & + // Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) rather than a + // WalaPlus endpoint. Maps whatever overlaps onto this model so the existing grid/card/detail + // screen can render it unchanged - fields with no equivalent on OffersListModel (favorite, + // redeem, locations, contact, ...) are simply left null, and isExclusive hides the actions + // that depend on them (see WalaPlusRetailerCard, RetailerDetailsScreen). + factory WalaPlusRetailerModel.fromExclusiveOffer(OffersListModel offer, {bool isAr = false}) { + String? name = decodeUnicodeString(isAr ? (offer.titleAr ?? offer.titleEn) : (offer.titleEn ?? offer.titleAr)); + String? description = decodeUnicodeString(isAr ? (offer.descriptionAr ?? offer.descriptionEn) : (offer.descriptionEn ?? offer.descriptionAr)); + String? categoryName = decodeUnicodeString(isAr ? (offer.categoryNameAr ?? offer.categoryNameEn) : (offer.categoryNameEn ?? offer.categoryNameAr)); + String? discountText = _formatExclusiveDiscountText(decodeUnicodeString(offer.discountDescription) ?? decodeUnicodeString(offer.discount)); + return WalaPlusRetailerModel( + id: "exclusive-${offer.offersDiscountId ?? ''}", + name: name, + // WalaPlusRetailerCard reads offerTitle as the promo subtitle shown under the name (e.g. + // "20% off") - the discount text is the closest equivalent on a legacy offer. + offerTitle: discountText, + description: description, + info: description, + image: offer.bannerImage, + logo: offer.logo, + website: offer.websiteUrl, + endDate: offer.endDate, + validTo: offer.endDate, + categoryName: categoryName, + categories: categoryName != null ? [WalaPlusCategoryModel(name: categoryName)] : null, + isExclusive: true, + ); + } + + // The legacy Offers & Discounts API sometimes returns a bare number ("10", "20") for its + // discount fields rather than a formatted "10% off" string. WalaPlusRetailerCard's badge + // logic (getDiscountBadge) only recognizes a "%" pattern, so a bare number would + // otherwise render verbatim instead of as a discount. Already-formatted text (containing "%", + // "off", or "خصم") is left untouched. + static String? _formatExclusiveDiscountText(String? raw) { + if (raw == null) return null; + String trimmed = raw.trim(); + if (trimmed.isEmpty) return null; + if (trimmed.contains('%') || trimmed.toLowerCase().contains('off') || trimmed.contains('خصم')) { + return trimmed; + } + if (RegExp(r'^\d+(\.\d+)?$').hasMatch(trimmed)) { + return LocaleKeys.offDiscount.tr(args: ['$trimmed%']); + } + return trimmed; + } + static String? decodeUnicodeString(Object? val) { if (val == null) return null; String s = val.toString().trim(); @@ -614,6 +661,7 @@ class WalaPlusRetailerModel { data['country_name_en'] = countryNameEn; data['is_favorite'] = isFavorite; data['isfavorite'] = isFavorite; + data['is_exclusive'] = isExclusive; if (offers != null) { data['offers'] = offers!.map((v) => v.toJson()).toList(); } @@ -633,9 +681,8 @@ class WalaPlusRetailerModel { for (var cat in categories!) { String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); - String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null") - ? primary.trim() - : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + String? resolved = + (primary != null && primary.trim().isNotEmpty && primary != "null") ? primary.trim() : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); if (resolved != null && resolved.isNotEmpty) { return resolved; } @@ -649,9 +696,10 @@ class WalaPlusRetailerModel { if (cat.id?.toString() == categoryId?.toString()) { String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); - String? resolved = (primary != null && primary.trim().isNotEmpty && primary != "null") - ? primary.trim() - : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + String? resolved = + (primary != null && primary.trim().isNotEmpty && primary != "null") + ? primary.trim() + : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); if (resolved != null && resolved.isNotEmpty) { return resolved; } diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 065ebf5..b1337da 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -460,7 +460,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb InkWell( borderRadius: BorderRadius.circular(20), onTap: () { - Navigator.pushNamed(context, AppRoutes.dashboardScreenForDependet); + Navigator.pushNamed(context, AppRoutes.mazayaDiscover); }, child: ClipRRect( borderRadius: BorderRadius.circular(20), diff --git a/lib/ui/landing/widget/services_widget.dart b/lib/ui/landing/widget/services_widget.dart index f1e03f8..74cfc0f 100644 --- a/lib/ui/landing/widget/services_widget.dart +++ b/lib/ui/landing/widget/services_widget.dart @@ -111,7 +111,7 @@ class ServicesWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true)), + Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true, maxLine: 3)), RotatedBox(quarterTurns: AppState().isArabic(context) ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)), ], ), diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart index fa83d3d..f6cb4ab 100644 --- a/lib/ui/login/login_screen.dart +++ b/lib/ui/login/login_screen.dart @@ -920,7 +920,7 @@ class _LoginScreenState extends State { if (effectiveToken.isNotEmpty && mounted) { Utils.hideLoading(context); - Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (Route route) => false); + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false); return true; } } catch (e) { @@ -990,7 +990,7 @@ class _LoginScreenState extends State { Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString()); if (navigateToDashboard && ApiConsts.ISDependetLogin) { - Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (Route route) => false); + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false); } } diff --git a/lib/ui/mazaya/mazaya_discover_screen.dart b/lib/ui/mazaya/mazaya_discover_screen.dart new file mode 100644 index 0000000..eec1603 --- /dev/null +++ b/lib/ui/mazaya/mazaya_discover_screen.dart @@ -0,0 +1,1022 @@ +import 'dart:async'; + +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/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.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/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_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'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart'; +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; + List retailers = []; + + // Resolved from the retailer response's own embedded `categories` data - fallback source for + // the title when the id isn't found in the top-level categories list (see _sectionTitle). + String? resolvedName; + + _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); + + @override + State createState() => _MazayaDiscoverScreenState(); +} + +class _MazayaDiscoverScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + + bool _isLoadingCategories = true; + List _categories = []; + + bool _isLoadingExclusiveOffers = true; + List _exclusiveOffers = []; + + String selectedCity = "All Cities"; + bool _isLoadingLatest = true; + List _latestRetailers = []; + + // 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('Q4'); + final _MazayaCategorySection _homeKitchenSection = _MazayaCategorySection( + 'Bw', + ); + + @override + void initState() { + super.initState(); + _fetchCategories(); + _fetchExclusiveOffers(); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + // Only drives the benefits strip now - Travel Essentials/Home & Kitchen/Latest on Mazaya each + // fetch independently (see _fetchCategorySection/_fetchLatestOnMazaya). Still fetched in + // parallel with those though, since it also feeds each fixed-category section's title (the + // top-level categories list is the primary source there - see _sectionTitle). + Future _fetchCategories() async { + setState(() => _isLoadingCategories = true); + try { + List categories = + await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); + if (!mounted) return; + setState(() { + _categories = categories; + _isLoadingCategories = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching categories: $e"); + if (mounted) setState(() => _isLoadingCategories = false); + } + } + + // 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 { + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: section.categoryId, + page: 1, + perPage: 10, + context: context, + ); + if (!mounted) return; + List retailers = + response?.retailers ?? []; + String? categoryName; + for (WalaPlusRetailerModel r in retailers) { + for (WalaPlusCategoryModel c + in r.categories ?? []) { + if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) { + categoryName = c.name; + break; + } + } + if (categoryName != null) break; + } + setState(() { + section.retailers = retailers; + section.resolvedName = categoryName; + section.isLoading = false; + }); + } catch (e) { + debugPrint( + "MazayaDiscoverScreen: error fetching category section '${section.categoryId}': $e", + ); + if (mounted) setState(() => section.isLoading = false); + } + } + + // "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 { + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + includeCategoryId: false, + sortBy: 'newest', + page: 1, + perPage: 10, + context: context, + ); + if (!mounted) return; + setState(() { + _latestRetailers = response?.retailers ?? []; + _isLoadingLatest = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching latest-on-Mazaya: $e"); + if (mounted) setState(() => _isLoadingLatest = false); + } + } + + // 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) { + for (WalaPlusCategoryModel c in _categories) { + if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) { + return c.name!; + } + } + if (section.resolvedName != null && section.resolvedName!.isNotEmpty) { + return section.resolvedName!; + } + 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, + ) { + for (WalaPlusCategoryModel c in _categories) { + if (c.id == section.categoryId) return c; + } + return WalaPlusCategoryModel( + id: section.categoryId, + name: _sectionTitle(section, fallbackTitle), + ); + } + + // "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 { + bool isAr = AppState().isArabic(context); + List offers = await OffersAndDiscountsApiClient() + .getOffersList(0, 10); + if (!mounted) return; + setState(() { + _exclusiveOffers = + offers + .map( + (o) => + WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr), + ) + .toList(); + _isLoadingExclusiveOffers = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching exclusive offers: $e"); + if (mounted) setState(() => _isLoadingExclusiveOffers = false); + } + } + + void _openRetailer(WalaPlusRetailerModel item) { + Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + } + + void _openRetailersGrid({WalaPlusCategoryModel? category}) { + Navigator.pushNamed( + context, + AppRoutes.retailersListScreen, + arguments: category, + ); + } + + // Exclusive offers reuse the legacy Offers & Discounts API (see fetchExclusiveOffers in both + // this screen and RetailersListScreen), so "See all" opens RetailersListScreen's exclusive + // mode instead of a WalaPlus category. + void _openExclusiveOffersGrid() { + Navigator.pushNamed( + context, + AppRoutes.retailersListScreen, + arguments: const {'exclusiveMode': true}, + ); + } + + Future _onRefresh() async { + await Future.wait(>[ + _fetchCategories(), + _fetchLatestOnMazaya(), + _fetchCategorySection(_travelSection), + _fetchCategorySection(_homeKitchenSection), + _fetchExclusiveOffers(), + ]); + } + + Future _clearDependentPrefs() async { + await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); + await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); + await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE"); + await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER"); + await Utils.removeStringFromPrefs("P_DEPENDENT_NAME"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE"); + } + + // Same app bar actions as the previous dependent-login landing page + // (DashboardScreenForDependent, now reached from here instead): language toggle + logout, + // shown only for dependent-login sessions. Tapping back when there's nothing to pop (this + // screen is now the dependent-login landing page, so that's the common case) logs out instead. + void _performLogout() { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.areYouSureWantToLogOut.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + isDestructive: false, + onConfirm: () async { + if (ApiConsts.isDependetLogin) { + await _clearDependentPrefs(); + } + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + AppState().setDependentName = null; + AppState().setDependentDob = null; + AppState().setDependentLoginType = null; + AppState().setWalaplusDepInfoModel = null; + AppState().setPostParamsInitConfig(); + if (mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (Route route) => false, + ); + } + }, + ); + } + + Future _changeLanguage(Locale newLocale) async { + if (context.locale == newLocale) return; + await context.setLocale(newLocale); + PostParamsModel? obj = AppState().postParamsObject; + if (obj != null) { + obj.languageID = (newLocale.languageCode == "ar" ? 1 : 2); + AppState().setPostParamsModel(obj); + } + if (mounted) { + setState(() {}); + await _onRefresh(); + } + } + + Widget _buildLanguageIconButton(bool isAr) { + return Tooltip( + message: isAr ? 'English' : 'العربية', + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + if (isAr) { + _changeLanguage(const Locale("en", "US")); + } else { + _changeLanguage(const Locale("ar", "SA")); + } + }, + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.language_rounded, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.mazayaDeals.tr(), + showHomeButton: false, + onBackTapped: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else if (ApiConsts.isDependetLogin) { + _performLogout(); + } + }, + actions: + ApiConsts.isDependetLogin + ? [ + _buildLanguageIconButton(isAr), + const SizedBox(width: 4), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _performLogout, + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + 'assets/images/drawer/logout.svg', + height: 20, + width: 20, + ), + ), + ), + const SizedBox(width: 12), + ] + : null, + ), + bottomNavigationBar: const MazayaBottomNav( + currentTab: MazayaTab.discover, + ), + body: RefreshIndicator( + onRefresh: _onRefresh, + color: MyColors.textMixColor, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(22, 16, 22, 0), + child: _buildSearchField(), + ), + const SizedBox(height: 18), + _buildCategoriesSection(), + const SizedBox(height: 24), + _buildExclusiveOffersSection(), + const SizedBox(height: 24), + _buildBrandSection( + title: LocaleKeys.latestOnMazaya.tr(), + subtitle: LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr(), + isLoading: _isLoadingLatest, + retailers: _latestRetailers, + backgroundColor: Colors.white, + onSeeAll: + () => _openRetailersGrid( + category: WalaPlusCategoryModel( + id: 'all', + name: LocaleKeys.allCategories.tr(), + ), + ), + ), + _buildBrandSection( + title: _sectionTitle( + _travelSection, + LocaleKeys.travelEssentials.tr(), + ), + subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), + isLoading: _travelSection.isLoading, + retailers: _travelSection.retailers, + backgroundColor: const Color(0xFFEFF2EF), + onSeeAll: + () => _openRetailersGrid( + category: _categoryForSection( + _travelSection, + LocaleKeys.travelEssentials.tr(), + ), + ), + ), + _buildBrandSection( + title: _sectionTitle( + _homeKitchenSection, + LocaleKeys.homeAndKitchen.tr(), + ), + subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(), + isLoading: _homeKitchenSection.isLoading, + retailers: _homeKitchenSection.retailers, + backgroundColor: Colors.white, + onSeeAll: + () => _openRetailersGrid( + category: _categoryForSection( + _homeKitchenSection, + LocaleKeys.homeAndKitchen.tr(), + ), + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ), + ); + } + + 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, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + hintText: LocaleKeys.findABrandOrSomething.tr(), + hintStyle: const TextStyle( + color: Color(0xFF78817F), + fontSize: 12, + ), + ), + style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)), + onTap: _openRetailersGrid, + readOnly: true, + ), + ), + ], + ), + ); + } + + Widget _buildCategoriesSection() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.exploreYourBenefits.tr().toText12( + color: const Color(0xFF78817F), + weight: FontWeight.w500, + ), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: + () => showMazayaCitySelectionBottomSheet( + context, + selectedCity: selectedCity, + onCitySelected: + (city) => setState(() => selectedCity = city), + ), + 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.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(height: 16), + SizedBox( + height: 112, + child: + _isLoadingCategories + ? Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 6, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 16), + itemBuilder: + (BuildContext _, int __) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 8), + Container( + width: 60, + height: 10, + color: Colors.white, + ), + ], + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: _categories.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 16), + itemBuilder: (BuildContext _, int index) { + WalaPlusCategoryModel category = _categories[index]; + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => _openRetailersGrid(category: category), + child: SizedBox( + width: 72, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (category.image != null && + category.image!.isNotEmpty) + ? Image.network( + category.image!, + width: 72, + height: 72, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + width: 72, + height: 72, + color: const Color( + 0xFFF1F5F9, + ), + child: const Icon( + Icons.category_outlined, + color: Color(0xFF94A3B8), + ), + ), + ) + : Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.category_outlined, + color: Color(0xFF94A3B8), + ), + ), + ), + const SizedBox(height: 8), + (category.name ?? '').toText10( + color: const Color(0xFF111111), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildExclusiveOffersSection() { + return Container( + width: double.infinity, + color: const Color(0xFF143F36), + padding: const EdgeInsets.only(top: 23, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: LocaleKeys.theMazayaEdit.tr().toText10( + color: const Color(0xFFDCC58F), + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.exclusivelyYours.tr().toText20( + color: Colors.white, + weight: FontWeight.w600, + ), + InkWell( + onTap: _openExclusiveOffersGrid, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.seeAll.tr().toText11( + color: const Color(0xFFF1EBDD), + weight: FontWeight.w500, + ), + const SizedBox(width: 4), + SvgPicture.asset( + "assets/icons/mazaya_arrow_right.svg", + width: 15, + height: 15, + colorFilter: const ColorFilter.mode( + Color(0xFFF1EBDD), + BlendMode.srcIn, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + LocaleKeys.aLittleExtraJustForMazayaMembers.tr().toText11( + color: const Color(0xFFC2D4CC), + weight: FontWeight.w400, + ), + ], + ), + ), + const SizedBox(height: 17), + SizedBox( + height: 206, + child: + _isLoadingExclusiveOffers + ? Shimmer.fromColors( + baseColor: const Color(0xFF1B4C41), + highlightColor: const Color(0xFF225B4E), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: 2, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 12), + itemBuilder: + (BuildContext _, int __) => Container( + width: 260, + height: 206, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: _exclusiveOffers.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 12), + itemBuilder: + (BuildContext _, int index) => + _buildExclusiveOfferCard(_exclusiveOffers[index]), + ), + ), + ], + ), + ); + } + + Widget _buildExclusiveOfferCard(WalaPlusRetailerModel offer) { + String? imageUrl = offer.displayImage; + String offerText = offer.offerTitle ?? offer.description ?? ''; + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _openRetailer(offer), + child: Container( + width: 260, + height: 206, + decoration: BoxDecoration( + color: const Color(0xFFF1EBDD), + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 128, + width: double.infinity, + child: + (imageUrl != null && imageUrl.isNotEmpty) + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFE4DCC8), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF9C8F6E), + ), + ), + ) + : Container( + color: const Color(0xFFE4DCC8), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF9C8F6E), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + (offer.name ?? '').toText13( + color: const Color(0xFF143F36), + weight: FontWeight.w600, + maxlines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), + offerText.toText12( + color: const Color(0xFF143F36), + weight: FontWeight.w400, + maxLine: 1, + ), + ], + ), + ), + SvgPicture.asset( + "assets/icons/mazaya_arrow_up_right.svg", + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + Color(0xFF143F36), + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBrandSection({ + required String title, + required String subtitle, + required bool isLoading, + required List retailers, + required Color backgroundColor, + required VoidCallback onSeeAll, + }) { + if (!isLoading && retailers.isEmpty) return const SizedBox.shrink(); + return Container( + width: double.infinity, + color: backgroundColor, + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title.toText20( + color: const Color(0xFF202F32), + weight: FontWeight.w600, + ), + InkWell( + onTap: onSeeAll, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.seeAll.tr().toText11( + color: const Color(0xFF147D6B), + weight: FontWeight.w500, + ), + const SizedBox(width: 4), + SvgPicture.asset( + "assets/icons/mazaya_arrow_right.svg", + width: 15, + height: 15, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + subtitle.toText11( + color: const Color(0xFF78817F), + weight: FontWeight.w400, + ), + ], + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 110, + child: + isLoading + ? Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: 4, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 20), + itemBuilder: + (BuildContext _, int __) => const Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 36, + backgroundColor: Colors.white, + ), + ], + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: retailers.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 20), + itemBuilder: + (BuildContext _, int index) => + _buildBrandItem(retailers[index]), + ), + ), + ], + ), + ); + } + + Widget _buildBrandItem(WalaPlusRetailerModel item) { + String? logoUrl = + (item.logoThumb != null && item.logoThumb!.isNotEmpty) + ? item.logoThumb + : item.logo; + return InkWell( + onTap: () => _openRetailer(item), + borderRadius: BorderRadius.circular(36), + child: SizedBox( + width: 80, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(36), + child: + (logoUrl != null && logoUrl.isNotEmpty) + ? Image.network( + logoUrl, + width: 72, + height: 72, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.storefront_outlined, + color: Color(0xFF94A3B8), + ), + ), + ) + : Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.storefront_outlined, + color: Color(0xFF94A3B8), + ), + ), + ), + const SizedBox(height: 6), + (item.name ?? '').toText11( + color: const Color(0xFF202F32), + weight: FontWeight.w600, + maxLine: 1, + overflow: TextOverflow.ellipsis, + isCenter: true, + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/mazaya/retailer_details_screen.dart similarity index 56% rename from lib/ui/landing/wala_plus_retailer_details_screen.dart rename to lib/ui/mazaya/retailer_details_screen.dart index 5ac2792..2c0f286 100644 --- a/lib/ui/landing/wala_plus_retailer_details_screen.dart +++ b/lib/ui/mazaya/retailer_details_screen.dart @@ -28,18 +28,23 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:shimmer/shimmer.dart'; import 'package:url_launcher/url_launcher.dart'; -class WalaPlusRetailerDetailsScreen extends StatefulWidget { +class RetailerDetailsScreen extends StatefulWidget { final WalaPlusRetailerModel? retailer; final String? retailerId; final String? userReference; - const WalaPlusRetailerDetailsScreen({Key? key, this.retailer, this.retailerId, this.userReference}) : super(key: key); + const RetailerDetailsScreen({ + Key? key, + this.retailer, + this.retailerId, + this.userReference, + }) : super(key: key); @override - State createState() => _WalaPlusRetailerDetailsScreenState(); + State createState() => _RetailerDetailsScreenState(); } -class _WalaPlusRetailerDetailsScreenState extends State { +class _RetailerDetailsScreenState extends State { WalaPlusRetailerModel? _retailer; bool _isLoading = true; bool _hasDetailsData = false; @@ -91,7 +96,14 @@ class _WalaPlusRetailerDetailsScreenState extends State AppInAppWebViewScreen(url: url.startsWith("http") ? url : "https://$url", title: _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr()), + builder: + (BuildContext context) => AppInAppWebViewScreen( + url: url.startsWith("http") ? url : "https://$url", + title: + _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr(), + ), ), ); } catch (err) { @@ -229,9 +285,17 @@ class _WalaPlusRetailerDetailsScreenState extends State AppInAppWebViewScreen(url: url, title: title, onPageLoaded: shouldStartScanning ? _clickStartScanningButton : null)), + MaterialPageRoute( + builder: + (BuildContext context) => AppInAppWebViewScreen( + url: url, + title: title, + onPageLoaded: + shouldStartScanning ? _clickStartScanningButton : null, + ), + ), ); } @@ -410,8 +509,14 @@ class _WalaPlusRetailerDetailsScreenState extends State _redeemOffer({required String offerId, required String offerType}) async { + Future _redeemOffer({ + required String offerId, + required String offerType, + }) async { if (_isRedeeming) return; if (offerId.isEmpty) { Utils.showToast(LocaleKeys.offerIdNotAvailable.tr()); @@ -436,13 +547,24 @@ class _WalaPlusRetailerDetailsScreenState extends State? res = await OffersAndDiscountsApiClient().redeemWalaPlusOffer(offerId: offerId, offerType: offerType, context: context); + Map? res = await OffersAndDiscountsApiClient() + .redeemWalaPlusOffer( + offerId: offerId, + offerType: offerType, + context: context, + ); Utils.hideLoading(context); String? transactionUrl; if (res != null) { - transactionUrl = res['Transaction_Url'] ?? res['TransactionUrl'] ?? res['transaction_url'] ?? res['transactionUrl'] ?? res['Url'] ?? res['url']; + transactionUrl = + res['Transaction_Url'] ?? + res['TransactionUrl'] ?? + res['transaction_url'] ?? + res['transactionUrl'] ?? + res['Url'] ?? + res['url']; } log("transactionUrl: ${transactionUrl.toString()}"); @@ -450,7 +572,11 @@ class _WalaPlusRetailerDetailsScreenState extends State offers) { - bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context); + bool isAr = + Directionality.of(context) == ui.TextDirection.rtl || + AppState().isArabic(context); showModalBottomSheet( context: context, @@ -481,14 +609,28 @@ class _WalaPlusRetailerDetailsScreenState extends State Navigator.pop(sheetContext), padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -526,7 +677,11 @@ class _WalaPlusRetailerDetailsScreenState extends State 14.height, itemBuilder: (BuildContext _, int index) { WalaPlusOfferModel offer = offers[index]; - return _buildOfferBottomSheetCard(sheetContext, offer, isAr); + return _buildOfferBottomSheetCard( + sheetContext, + offer, + isAr, + ); }, ), ), @@ -537,19 +692,33 @@ class _WalaPlusRetailerDetailsScreenState extends State(Color(0xFF168A89)), - ), - Expanded( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 1. Breadcrumbs (Category / Company Name) - _buildBreadcrumbs(isAr), - 12.height, + body: + (_isLoading && !_hasDetailsData) + ? _buildDetailsShimmer() + : Column( + children: [ + if (_isLoading && _hasDetailsData) + const LinearProgressIndicator( + minHeight: 2.5, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Color(0xFF168A89), + ), + ), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Breadcrumbs (Category / Company Name) + _buildBreadcrumbs(isAr), + 12.height, - // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container) - _buildHeroCard(isAr), - 14.height, + // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container) + _buildHeroCard(isAr), + 14.height, - // 3. Store Type & City Filter Chips - _buildChipsRow(isAr), - 20.height, + // 3. Store Type & City Filter Chips + _buildChipsRow(isAr), + 20.height, - // 4. Merchant Contact (Website, Phone, Email) - _buildMerchantContactSection(isAr), + // 4. Merchant Contact (Website, Phone, Email) + _buildMerchantContactSection(isAr), - // 5. Locations Section (Preview Cards + View All) - _buildLocationsSection(isAr), + // 5. Locations Section (Preview Cards + View All) + _buildLocationsSection(isAr), - // 6. Good to Know Section - _buildGoodToKnowSection(isAr), + // 6. Good to Know Section + _buildGoodToKnowSection(isAr), - 20.height, - ], + 20.height, + ], + ), ), ), - ), - // 7. Bottom Redeem Offer Action Bar - _buildBottomActionBar(isAr), - ], - ), + // 7. Bottom Redeem Offer Action Bar - not shown for exclusive offers (mapped + // from the legacy Offers & Discounts API), since there's no real WalaPlus + // offer/retailer ID behind them for redeemWalaPlusOffer to act on. + if (_retailer?.isExclusive != true) + _buildBottomActionBar(isAr), + ], + ), ); } // Skeleton mirroring the real card layout (breadcrumbs, hero card, chips, contact, locations) Widget _buildDetailsShimmer() { Widget block({double? width, double height = 14, double radius = 4}) { - return Container(width: width, height: height, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(radius))); + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(radius), + ), + ); } return Shimmer.fromColors( @@ -673,7 +889,10 @@ class _WalaPlusRetailerDetailsScreenState extends State[ - block(width: 80, height: 32, radius: 20), - const SizedBox(width: 8), - block(width: 80, height: 32, radius: 20), - const SizedBox(width: 8), - block(width: 100, height: 32, radius: 20), - ]), + Row( + children: [ + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 100, height: 32, radius: 20), + ], + ), 20.height, // Merchant contact placeholder block(width: 140, height: 18), @@ -724,7 +945,10 @@ class _WalaPlusRetailerDetailsScreenState extends State Container( + child: + (_retailer?.displayImage != null && + _retailer!.displayImage!.isNotEmpty) + ? Image.network( + _retailer!.displayImage!, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFF3F4F6), + child: const Icon( + Icons.broken_image, + size: 48, + color: Color(0xFF9CA3AF), + ), + ), + ) + : Container( color: const Color(0xFFF3F4F6), - child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF)), + child: const Icon( + Icons.image_outlined, + size: 48, + color: Color(0xFF9CA3AF), + ), ), - ) - : Container( - color: const Color(0xFFF3F4F6), - child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF)), - ), ), - PositionedDirectional( - top: 14, - end: 14, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.14), blurRadius: 8, offset: const Offset(0, 2)), - ], - ), - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: Icon( - _isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, - size: 22, - color: _isFavorite ? Colors.black : const Color(0xFF1E293B), + // Exclusive offers (mapped from the legacy Offers & Discounts API) have no + // WalaPlus retailer ID, so there's nothing for add/removeWalaPlusFavorite to + // act on - hide the button rather than wiring it to a call that can't work. + if (_retailer?.isExclusive != true) + PositionedDirectional( + top: 14, + end: 14, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.14), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + _isFavorite + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + size: 22, + color: + _isFavorite + ? Colors.black + : const Color(0xFF1E293B), + ), + tooltip: LocaleKeys.favorite.tr(), + onPressed: + _isTogglingFavorite ? null : _toggleFavorite, ), - tooltip: LocaleKeys.favorite.tr(), - onPressed: _isTogglingFavorite ? null : _toggleFavorite, ), ), - ), ], ), ), @@ -826,14 +1090,26 @@ class _WalaPlusRetailerDetailsScreenState extends State chips = []; + // Exclusive offers (mapped from the legacy Offers & Discounts API) carry no real + // type/locations/offers data, so there's nothing to infer Online/In Store from - the + // "neither known -> assume In Store" fallback below would otherwise show a wrong chip. + bool isExclusive = _retailer?.isExclusive == true; + List types = _retailer?.normalizedTypes ?? []; - bool hasOnline = types.contains('online') || (_retailer?.isOnlineStore == true); - bool hasInstore = types.contains('instore') || (_retailer?.isInstore == true) || (_retailer?.hasPhysicalLocation == true) || (_retailer?.locations != null && _retailer!.locations!.isNotEmpty); + bool hasOnline = + types.contains('online') || (_retailer?.isOnlineStore == true); + bool hasInstore = + types.contains('instore') || + (_retailer?.isInstore == true) || + (_retailer?.hasPhysicalLocation == true) || + (_retailer?.locations != null && _retailer!.locations!.isNotEmpty); String rType = (_retailer?.type ?? '').toLowerCase(); String rOfferType = (_retailer?.offerType ?? '').toLowerCase(); - if (rType.contains('online') || rOfferType.contains('online')) hasOnline = true; - if (rType.contains('store') || rOfferType.contains('store')) hasInstore = true; + if (rType.contains('online') || rOfferType.contains('online')) + hasOnline = true; + if (rType.contains('store') || rOfferType.contains('store')) + hasInstore = true; if (rType.isNotEmpty && !rType.contains('online')) hasInstore = true; if (_retailer?.offers != null) { for (var offer in _retailer!.offers!) { String oType = (offer.type ?? '').toLowerCase(); if (oType.contains('online')) hasOnline = true; - if (oType.contains('store') || (oType.isNotEmpty && !oType.contains('online'))) hasInstore = true; + if (oType.contains('store') || + (oType.isNotEmpty && !oType.contains('online'))) + hasInstore = true; } } - if (!hasOnline && !hasInstore) { + if (!isExclusive && !hasOnline && !hasInstore) { hasInstore = true; } + if (isExclusive) { + hasOnline = false; + hasInstore = false; + } // Online chip if (hasOnline) { @@ -876,9 +1170,11 @@ class _WalaPlusRetailerDetailsScreenState extends State _showAllLocationsBottomSheet(_retailer!.locations!, isAr), + onTap: + () => _showAllLocationsBottomSheet(_retailer!.locations!, isAr), borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), @@ -948,7 +1250,9 @@ class _WalaPlusRetailerDetailsScreenState extends State(Colors.white)), + child: + _isRedeeming + ? const Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + ) + : Row( + children: [ + LocaleKeys.redeemOffer.tr().toText16( + isBold: true, + color: Colors.white, + isCentered: false, + ), + const Spacer(), + Icon( + isAr + ? Icons.arrow_back_rounded + : Icons.arrow_forward_rounded, + color: Colors.white, + size: 20, + ), + ], ), - ) - : Row( - children: [ - LocaleKeys.redeemOffer.tr().toText16(isBold: true, color: Colors.white, isCentered: false), - const Spacer(), - Icon(isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, color: Colors.white, size: 20), - ], - ), ), ), ), @@ -1188,7 +1605,8 @@ class _WalaPlusRetailerDetailsScreenState extends State previewLocations = totalCount <= 2 ? locations : locations.take(2).toList(); + List previewLocations = + totalCount <= 2 ? locations : locations.take(2).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1197,7 +1615,10 @@ class _WalaPlusRetailerDetailsScreenState extends State _showAllLocationsBottomSheet(locations, isAr), borderRadius: BorderRadius.circular(6), @@ -1206,9 +1627,18 @@ class _WalaPlusRetailerDetailsScreenState extends State _openMapForLocation(loc), @@ -1252,7 +1685,11 @@ class _WalaPlusRetailerDetailsScreenState extends State locations, bool isAr) { + void _showAllLocationsBottomSheet( + List locations, + bool isAr, + ) { Map cityCounts = {}; for (var loc in locations) { String c = loc.getLocalizedCity(isAr); @@ -1294,7 +1749,9 @@ class _WalaPlusRetailerDetailsScreenState extends State sortedCities = cityCounts.keys.toList()..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0)); + List sortedCities = + cityCounts.keys.toList() + ..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0)); String selectedCity = "ALL"; String searchQuery = ""; @@ -1320,7 +1777,10 @@ class _WalaPlusRetailerDetailsScreenState extends State Navigator.pop(sheetContext), padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -1370,20 +1853,36 @@ class _WalaPlusRetailerDetailsScreenState extends State 10.height, itemBuilder: (BuildContext _, int index) { - return _buildLocationCard(filtered[index], isAr); + return _buildLocationCard( + filtered[index], + isAr, + ); }, ), ), @@ -1495,17 +2046,25 @@ class _WalaPlusRetailerDetailsScreenState extends State _openMapForLocation(WalaPlusRetailerLocation loc) async { - bool hasCoordinates = loc.latitude != null && loc.longitude != null && loc.latitude != 0 && loc.longitude != 0; + bool hasCoordinates = + loc.latitude != null && + loc.longitude != null && + loc.latitude != 0 && + loc.longitude != 0; Uri googleMapsUrl; if (hasCoordinates) { - googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}"); + googleMapsUrl = Uri.parse( + "https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}", + ); } else { String query = "${loc.address ?? ''} ${loc.city ?? ''}".trim(); if (query.isEmpty) { query = _retailer?.name ?? ""; } - googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}"); + googleMapsUrl = Uri.parse( + "https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}", + ); } try { diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/mazaya/retailers_list_screen.dart similarity index 55% rename from lib/ui/landing/dashboard_screen_for_dependet.dart rename to lib/ui/mazaya/retailers_list_screen.dart index 2da332c..e1c65d1 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/mazaya/retailers_list_screen.dart @@ -14,11 +14,13 @@ import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; -import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/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_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'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/info_chip_widget.dart'; @@ -26,23 +28,30 @@ import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; import 'package:permission_handler/permission_handler.dart'; import 'package:shimmer/shimmer.dart'; -class DashboardScreenForDependet extends StatefulWidget { - const DashboardScreenForDependet({Key? key}) : super(key: key); +class RetailersListScreen extends StatefulWidget { + const RetailersListScreen({Key? key}) : super(key: key); @override - State createState() => _DashboardScreenForDependetState(); + State createState() => _RetailersListScreenState(); } -class _DashboardScreenForDependetState extends State { +class _RetailersListScreenState extends State { List retailersList = []; List _filteredRetailersList = []; List categoriesList = []; WalaPlusCategoryModel? selectedCategory; String currentCategoryId = ""; String selectedTypeFilter = "all"; - String selectedCity = "Riyadh"; + String selectedCity = "All Cities"; bool _isInitRouteArgs = false; + // Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts + // (ITG) API/implementation (lib/ui/screens/offers_and_discounts) instead of a WalaPlus + // endpoint, so fetchCategories/fetchOffers/fetchSubcategories all short-circuit to + // fetchExclusiveOffers below rather than touching any WalaPlus-specific state or UI. + bool isExclusiveMode = false; + List _exclusiveOffers = []; + // Subcategories of whichever main category is currently selected - fetched via // getWalaPlusCategories(parentId: ...) whenever a main category card is tapped. Empty/cleared // when "All Categories" (or nothing) is selected, since there's no specific parent to ask for. @@ -94,7 +103,9 @@ class _DashboardScreenForDependetState extends State if (!_isInitRouteArgs) { _isInitRouteArgs = true; Object? args = ModalRoute.of(context)?.settings.arguments; - if (args is WalaPlusCategoryModel) { + if (args is Map && args['exclusiveMode'] == true) { + isExclusiveMode = true; + } else if (args is WalaPlusCategoryModel) { selectedCategory = args; currentCategoryId = args.id ?? ""; } else if (args is Map && args['category'] is WalaPlusCategoryModel) { @@ -116,19 +127,27 @@ class _DashboardScreenForDependetState extends State String name = AppState().dependentName ?? ""; String mobile = AppState().postParamsObject?.mobileNumber ?? ""; String tokenID = - (AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) ? AppState().postParamsObject!.tokenID! : (AppState().authenticationTokenID ?? ""); + (AppState().postParamsObject?.tokenID != null && + AppState().postParamsObject!.tokenID!.isNotEmpty) + ? AppState().postParamsObject!.tokenID! + : (AppState().authenticationTokenID ?? ""); if (tokenID.isEmpty) { tokenID = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID"); } int sessionId = AppState().postParamsObject?.pSessionId ?? 0; String email = AppState().postParamsObject?.pEmailAddress ?? ""; - String deviceToken = AppState().getIsHuawei - ? AppState().getHuaweiPushToken - : ((AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") - ? AppState().getDeviceToken! - : ""); + String deviceToken = + AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : ((AppState().getDeviceToken != null && + AppState().getDeviceToken!.isNotEmpty && + AppState().getDeviceToken != "null") + ? AppState().getDeviceToken! + : ""); if (deviceToken.isEmpty || deviceToken == "null") { - String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + String savedToken = await Utils.getStringFromPrefs( + "P_DEP_DEVICE_TOKEN", + ); if (savedToken.isNotEmpty && savedToken != "null") { deviceToken = savedToken; } @@ -154,13 +173,18 @@ class _DashboardScreenForDependetState extends State } void _loadDependentName() { - if (AppState().dependentName != null && AppState().dependentName!.isNotEmpty && mounted) { + if (AppState().dependentName != null && + AppState().dependentName!.isNotEmpty && + mounted) { setState(() {}); } } String _cleanDependentName(String name) { - return name.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim(); + return name + .replaceAll(RegExp(r'\s*-\s*'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); } @override @@ -184,16 +208,73 @@ class _DashboardScreenForDependetState extends State } } + // 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. There's no query param on + // getOffersList, so search filters the already-fetched list client-side instead of re-fetching. + // Each result is mapped onto WalaPlusRetailerModel (see fromExclusiveOffer) so it renders + // through the exact same grid/card used everywhere else, flagged via isExclusive. + Future fetchExclusiveOffers({ + String? query, + bool forceRefresh = false, + }) async { + if (query != null) searchQuery = query; + if (forceRefresh) _exclusiveOffers = []; + setState(() { + isLoading = true; + hasMore = false; + }); + try { + if (_exclusiveOffers.isEmpty) { + bool isAr = AppState().isArabic(context); + List offers = await OffersAndDiscountsApiClient() + .getOffersList(0, 100); + _exclusiveOffers = + offers + .map( + (o) => + WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr), + ) + .toList(); + } + retailersList = _exclusiveOffers; + String q = searchQuery.trim().toLowerCase(); + _filteredRetailersList = + q.isEmpty + ? _exclusiveOffers + : _exclusiveOffers + .where((o) => (o.name ?? '').toLowerCase().contains(q)) + .toList(); + } catch (e) { + debugPrint("RetailersListScreen: error fetching exclusive offers: $e"); + _filteredRetailersList = []; + } finally { + if (mounted) { + setState(() { + isLoading = false; + isCategoriesLoading = false; + }); + } + } + } + Future fetchCategories() async { + if (isExclusiveMode) { + await fetchExclusiveOffers(); + return; + } setState(() { isCategoriesLoading = true; }); try { - List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + List list = await OffersAndDiscountsApiClient() + .getWalaPlusCategories(context: context); if (list.isEmpty) { await Future.delayed(const Duration(milliseconds: 600)); - list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + list = await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); } if (mounted) { @@ -210,14 +291,29 @@ class _DashboardScreenForDependetState extends State WalaPlusCategoryModel? defaultCat; for (WalaPlusCategoryModel cat in list) { String n = (cat.name ?? "").toLowerCase(); - if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) { + if (n.contains("travel") || + n.contains("entertain") || + n.contains("سفر") || + n.contains("ترفيه")) { defaultCat = cat; break; } } defaultCat ??= list.firstWhere( - (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع", - orElse: () => list.isNotEmpty ? list.first : WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment"), + (c) => + c.id != "all" && + c.id != "0" && + (c.name ?? "").toLowerCase() != "all" && + c.name != "الكل" && + c.name != "الجميع", + orElse: + () => + list.isNotEmpty + ? list.first + : WalaPlusCategoryModel( + id: "1", + name: "Travel & Entertainment", + ), ); selectedCategory = defaultCat; } @@ -234,7 +330,10 @@ class _DashboardScreenForDependetState extends State debugPrint("Error fetching WalaPlus categories (attempt 1): $e"); try { await Future.delayed(const Duration(milliseconds: 600)); - List retryList = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + List retryList = + await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); if (mounted && retryList.isNotEmpty) { categoriesList = retryList; if (selectedCategory != null) { @@ -248,13 +347,21 @@ class _DashboardScreenForDependetState extends State WalaPlusCategoryModel? defaultCat; for (WalaPlusCategoryModel cat in retryList) { String n = (cat.name ?? "").toLowerCase(); - if (n.contains("travel") || n.contains("entertain") || n.contains("سفر") || n.contains("ترفيه")) { + if (n.contains("travel") || + n.contains("entertain") || + n.contains("سفر") || + n.contains("ترفيه")) { defaultCat = cat; break; } } defaultCat ??= retryList.firstWhere( - (c) => c.id != "all" && c.id != "0" && (c.name ?? "").toLowerCase() != "all" && c.name != "الكل" && c.name != "الجميع", + (c) => + c.id != "all" && + c.id != "0" && + (c.name ?? "").toLowerCase() != "all" && + c.name != "الكل" && + c.name != "الجميع", orElse: () => retryList.first, ); selectedCategory = defaultCat; @@ -273,7 +380,10 @@ class _DashboardScreenForDependetState extends State if (mounted) { if (selectedCategory == null) { - selectedCategory = WalaPlusCategoryModel(id: "1", name: "Travel & Entertainment"); + selectedCategory = WalaPlusCategoryModel( + id: "1", + name: "Travel & Entertainment", + ); currentCategoryId = "1"; } setState(() {}); @@ -291,6 +401,7 @@ class _DashboardScreenForDependetState extends State // Fetches the subcategories under a main category (WP_ParentId). "All Categories" (or no // category) has no specific parent, so that just clears the list instead of calling the API. Future fetchSubcategories(String? parentId) async { + if (isExclusiveMode) return; if (parentId == null || parentId.isEmpty || parentId == "all") { setState(() { subcategoriesList = []; @@ -304,7 +415,8 @@ class _DashboardScreenForDependetState extends State }); try { - List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context, parentId: parentId); + List list = await OffersAndDiscountsApiClient() + .getWalaPlusCategories(context: context, parentId: parentId); if (mounted) { setState(() { subcategoriesList = list; @@ -336,7 +448,16 @@ class _DashboardScreenForDependetState extends State } Future fetchOffers({String? categoryId, String? query}) async { - String targetCategoryId = categoryId ?? _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId); + if (isExclusiveMode) { + await fetchExclusiveOffers(query: query); + return; + } + String targetCategoryId = + categoryId ?? + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); if (query != null) { searchQuery = query; } @@ -353,30 +474,42 @@ class _DashboardScreenForDependetState extends State }); try { - String catParam = (targetCategoryId == "all" || targetCategoryId == "0") ? "" : targetCategoryId; - String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter); - String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String catParam = + (targetCategoryId == "all" || targetCategoryId == "0") + ? "" + : targetCategoryId; + String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter( + selectedTypeFilter, + ); + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; int? radiusParam = _radiusFromFilterDistance(_filterDistance); - int? minDiscountParam = _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount); - print("Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize"); - WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( - categoryId: catParam, - page: 1, - perPage: pageSize, - offerType: offerTypeParam, - query: queryParam, - sortBy: _filterSortBy, - radius: radiusParam, - minDiscount: minDiscountParam, - context: wantsNearest ? context : null, - userReference: null, + int? minDiscountParam = + _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount); + print( + "Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize", ); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: catParam, + page: 1, + perPage: pageSize, + offerType: offerTypeParam, + query: queryParam, + sortBy: _filterSortBy, + radius: radiusParam, + minDiscount: minDiscountParam, + context: wantsNearest ? context : null, + userReference: null, + ); retailersList = response?.retailers ?? []; _filteredRetailersList = retailersList; currentPage = 1; totalPages = response?.pagesCount ?? 1; - hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false); + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); } catch (e) { print("Error fetching WalaPlus retailers: $e"); retailersList = []; @@ -406,26 +539,38 @@ class _DashboardScreenForDependetState extends State try { int nextPage = currentPage + 1; String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId; - String catParam = (effectiveCategoryId == "all" || effectiveCategoryId == "0") ? "" : effectiveCategoryId; - String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter); - String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String catParam = + (effectiveCategoryId == "all" || effectiveCategoryId == "0") + ? "" + : effectiveCategoryId; + String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter( + selectedTypeFilter, + ); + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; int? radiusParam = _radiusFromFilterDistance(_filterDistance); - int? minDiscountParam = (_filterMinDiscount == 'any') ? null : int.tryParse(_filterMinDiscount); - print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'"); - - WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( - categoryId: catParam, - page: nextPage, - perPage: pageSize, - offerType: offerTypeParam, - query: queryParam, - sortBy: _filterSortBy, - radius: radiusParam, - minDiscount: minDiscountParam, - context: wantsNearest ? context : null, - userReference: null, + int? minDiscountParam = + (_filterMinDiscount == 'any') + ? null + : int.tryParse(_filterMinDiscount); + print( + "Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'", ); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: catParam, + page: nextPage, + perPage: pageSize, + offerType: offerTypeParam, + query: queryParam, + sortBy: _filterSortBy, + radius: radiusParam, + minDiscount: minDiscountParam, + context: wantsNearest ? context : null, + userReference: null, + ); + List newRetailers = response?.retailers ?? []; if (newRetailers.isNotEmpty) { currentPage = nextPage; @@ -449,10 +594,24 @@ class _DashboardScreenForDependetState extends State } Future _onRefresh() async { - String effectiveCategoryId = _selectedSubcategoryId ?? (currentCategoryId.isEmpty ? (selectedCategory?.id ?? "") : currentCategoryId); + if (isExclusiveMode) { + await fetchExclusiveOffers( + query: _searchController.text, + forceRefresh: true, + ); + return; + } + String effectiveCategoryId = + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); await Future.wait(>[ fetchCategories(), - fetchOffers(categoryId: effectiveCategoryId, query: _searchController.text), + fetchOffers( + categoryId: effectiveCategoryId, + query: _searchController.text, + ), ]); } @@ -491,13 +650,16 @@ class _DashboardScreenForDependetState extends State Completer permissionCompleter = Completer(); location.Location.havePermission((bool hasPermission) { - if (!permissionCompleter.isCompleted) permissionCompleter.complete(hasPermission); + if (!permissionCompleter.isCompleted) + permissionCompleter.complete(hasPermission); }); bool hasPermission = await permissionCompleter.future; if (!hasPermission) { bool granted = await _requestLocationPermission(); if (!granted) { - Utils.showToast(LocaleKeys.locationPermissionRequiredForNearestOffers.tr()); + Utils.showToast( + LocaleKeys.locationPermissionRequiredForNearestOffers.tr(), + ); return false; } } @@ -505,8 +667,10 @@ class _DashboardScreenForDependetState extends State } Future _requestLocationPermission() async { - Map result = await [Permission.location].request(); - return result[Permission.location] == PermissionStatus.granted || result[Permission.locationAlways] == PermissionStatus.granted; + Map result = + await [Permission.location].request(); + return result[Permission.location] == PermissionStatus.granted || + result[Permission.locationAlways] == PermissionStatus.granted; } void _onSearchChanged(String query) { @@ -532,6 +696,7 @@ class _DashboardScreenForDependetState extends State setState(() { item.isFavorite = targetFav; }); + Utils.showLoading(context); try { GenericResponseModel? response; @@ -550,15 +715,23 @@ class _DashboardScreenForDependetState extends State ); } + Utils.hideLoading(context); + if (response != null && response.errorMessage == null) { bool isAr = AppState().isArabic(context); String msg; if (isAr) { - msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr(); + msg = + targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); } else { msg = response.wpFavoritesAddRemoveMessage ?? ""; if (msg.trim().isEmpty) { - msg = targetFav ? LocaleKeys.favoriteAddedSuccess.tr() : LocaleKeys.favoriteRemovedSuccess.tr(); + msg = + targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); } } Utils.showToast(msg, isSuccess: true); @@ -568,10 +741,14 @@ class _DashboardScreenForDependetState extends State item.isFavorite = currentFav; }); } - String error = response?.errorEndUserMessage ?? response?.errorMessage ?? LocaleKeys.favoriteError.tr(); + String error = + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(); Utils.showToast(error); } } catch (e) { + Utils.hideLoading(context); debugPrint("Error toggling favorite: $e"); if (mounted) { setState(() { @@ -595,7 +772,11 @@ class _DashboardScreenForDependetState extends State } } } - await Navigator.pushNamed(context, AppRoutes.walaPlusRetailerDetails, arguments: item); + await Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); if (mounted) { setState(() {}); } @@ -622,7 +803,10 @@ class _DashboardScreenForDependetState extends State await Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", mobile); } String tokenID = - (AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) ? AppState().postParamsObject!.tokenID! : (AppState().authenticationTokenID ?? ""); + (AppState().postParamsObject?.tokenID != null && + AppState().postParamsObject!.tokenID!.isNotEmpty) + ? AppState().postParamsObject!.tokenID! + : (AppState().authenticationTokenID ?? ""); if (tokenID.isNotEmpty) { await Utils.saveStringFromPrefs("P_DEP_TOKEN_ID", tokenID); } @@ -632,11 +816,17 @@ class _DashboardScreenForDependetState extends State } String authTokenID = AppState().authenticationTokenID ?? ""; if (authTokenID.isNotEmpty) { - await Utils.saveStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID", authTokenID); + await Utils.saveStringFromPrefs( + "P_DEP_AUTHENTICATION_TOKEN_ID", + authTokenID, + ); } int loginType = AppState().dependentLoginType ?? 1; await Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString()); - String deviceToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? ""); + String deviceToken = + AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : (AppState().getDeviceToken ?? ""); if (deviceToken.isNotEmpty) { await Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", deviceToken); } @@ -676,7 +866,11 @@ class _DashboardScreenForDependetState extends State AppState().setWalaplusDepInfoModel = null; AppState().setPostParamsInitConfig(); if (mounted) { - Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false); + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (Route route) => false, + ); } }, ); @@ -693,7 +887,9 @@ class _DashboardScreenForDependetState extends State if (mounted) { setState(() {}); await fetchCategories(); - await fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId); + await fetchOffers( + categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId, + ); } } @@ -709,7 +905,14 @@ class _DashboardScreenForDependetState extends State _changeLanguage(const Locale("ar", "SA")); } }, - child: const Padding(padding: EdgeInsets.all(3), child: Icon(Icons.language_rounded, size: 24, color: Color(0xFF1E293B))), + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.language_rounded, + size: 24, + color: Color(0xFF1E293B), + ), + ), ), ); } @@ -740,11 +943,18 @@ class _DashboardScreenForDependetState extends State }, child: const Padding( padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), - child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B)), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), ), ), const SizedBox(width: 8), - LocaleKeys.mazayaDeals.tr().toText22(isBold: true, color: const Color(0xFF202F31)), + LocaleKeys.mazayaDeals.tr().toText22( + isBold: true, + color: const Color(0xFF202F31), + ), ], ), ), @@ -757,7 +967,11 @@ class _DashboardScreenForDependetState extends State onTap: _performLogout, child: Padding( padding: const EdgeInsets.all(6), - child: SvgPicture.asset('assets/images/drawer/logout.svg', height: 20, width: 20), + child: SvgPicture.asset( + 'assets/images/drawer/logout.svg', + height: 20, + width: 20, + ), ), ), const SizedBox(width: 12), @@ -770,7 +984,9 @@ class _DashboardScreenForDependetState extends State color: MyColors.textMixColor, child: SingleChildScrollView( controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -784,38 +1000,22 @@ class _DashboardScreenForDependetState extends State // 3. Search Bar: "Find an escape or an experience" _buildSearchBar(isAr), - // 4. Filters & Subcategories Row - _buildSubcategoriesAndFilterRow(isAr), + // 4. Filters & Subcategories Row - the filters (sort/distance/discount) and + // subcategories are WalaPlus-specific concepts that don't apply to exclusive + // offers (legacy Offers & Discounts API), so this row is skipped in that mode. + if (!isExclusiveMode) _buildSubcategoriesAndFilterRow(isAr), const SizedBox(height: 16), // 5. Offers Grid - if (isLoading) - _buildRetailersShimmer() - else if (_filteredRetailersList.isNotEmpty) ...[ - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 4 : 2, - mainAxisExtent: 235, - crossAxisSpacing: 12, - mainAxisSpacing: 16, - ), - itemCount: _filteredRetailersList.length, - itemBuilder: (context, index) { - return _buildOfferCard(_filteredRetailersList[index], isAr); - }, - ), - if (isLoadingMore) ...[ - 16.height, - Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), - ], - ] else - Padding( - padding: const EdgeInsets.only(top: 40), - child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B))), - ), + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: _filteredRetailersList, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + ), ], ), ), @@ -830,7 +1030,9 @@ class _DashboardScreenForDependetState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.exploreYourBenefits.tr().toText13(color: const Color(0xFF79807F)), + LocaleKeys.exploreYourBenefits.tr().toText13( + color: const Color(0xFF79807F), + ), InkWell( borderRadius: BorderRadius.circular(8), onTap: _showCitySelectionBottomSheet, @@ -839,9 +1041,16 @@ class _DashboardScreenForDependetState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.location_on_outlined, size: 16, color: Color(0xFF157D6B)), + const Icon( + Icons.location_on_outlined, + size: 16, + color: Color(0xFF157D6B), + ), const SizedBox(width: 4), - selectedCity.toText13(isBold: true, color: const Color(0xFF157D6B)), + selectedCity.toText13( + isBold: true, + color: const Color(0xFF157D6B), + ), ], ), ), @@ -852,8 +1061,22 @@ class _DashboardScreenForDependetState extends State } Widget _buildCategoryHero(bool isAr) { - String categoryName = selectedCategory?.name ?? (isAr ? "السفر والترفيه" : "Travel & Entertainment"); - String subtitle = isAr ? "مغامرتك القادمة، مع القليل من التميز." : "Your next adventure, with a little extra."; + bool isLatest = selectedCategory?.id == 'all'; + String categoryName = + isExclusiveMode + ? LocaleKeys.exclusivelyYours.tr() + : isLatest + ? LocaleKeys.latestOnMazaya.tr() + : selectedCategory?.name ?? + (isAr ? "السفر والترفيه" : "Travel & Entertainment"); + String subtitle = + isExclusiveMode + ? LocaleKeys.aLittleExtraJustForMazayaMembers.tr() + : isLatest + ? LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr() + : (isAr + ? "مغامرتك القادمة، مع القليل من التميز." + : "Your next adventure, with a little extra."); return Padding( padding: const EdgeInsets.only(bottom: 16), @@ -864,24 +1087,55 @@ class _DashboardScreenForDependetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - categoryName.toText24(isBold: true, color: const Color(0xFF202F31)), + categoryName.toText24( + isBold: true, + color: const Color(0xFF202F31), + ), const SizedBox(height: 6), subtitle.toText12(color: const Color(0xFF7A8180)), ], ), ), const SizedBox(width: 16), - _buildCategoryHeroImage(selectedCategory), + _buildCategoryHeroImage( + selectedCategory, + isLatest: isLatest, + isExclusive: isExclusiveMode, + ), ], ), ); } - Widget _buildCategoryHeroImage(WalaPlusCategoryModel? category) { + Widget _buildCategoryHeroImage( + WalaPlusCategoryModel? category, { + bool isLatest = false, + bool isExclusive = false, + }) { const double imgWidth = 84.0; const double imgHeight = 84.0; const double radius = 18.0; + if (isExclusive) { + return _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + isExclusive: true, + ); + } + + if (isLatest) { + return _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + isLatest: true, + ); + } + String? imgUrl = category?.categoryImageUrl; if (imgUrl != null && imgUrl.isNotEmpty) { if (imgUrl.toLowerCase().endsWith('.svg') || (category?.isSvg ?? false)) { @@ -893,7 +1147,13 @@ class _DashboardScreenForDependetState extends State child: SvgPicture.network( imgUrl, fit: BoxFit.cover, - placeholderBuilder: (_) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius), + placeholderBuilder: + (_) => _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + ), ), ), ); @@ -906,7 +1166,13 @@ class _DashboardScreenForDependetState extends State child: Image.network( imgUrl, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius), + errorBuilder: + (_, __, ___) => _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + ), ), ), ); @@ -914,21 +1180,41 @@ class _DashboardScreenForDependetState extends State return _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius); } - Widget _buildCategoryHeroFallback(WalaPlusCategoryModel? category, double w, double h, double r) { + Widget _buildCategoryHeroFallback( + WalaPlusCategoryModel? category, + double w, + double h, + double r, { + bool isLatest = false, + bool isExclusive = false, + }) { return ClipRRect( borderRadius: BorderRadius.circular(r), child: Container( width: w, height: h, - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF38BDF8), Color(0xFF0284C7)], + colors: + isExclusive + ? const [Color(0xFF143F36), Color(0xFF0B2A23)] + : isLatest + ? const [Color(0xFF1FA98D), Color(0xFF147D6B)] + : const [Color(0xFF38BDF8), Color(0xFF0284C7)], ), ), - child: const Center( - child: Icon(Icons.flight_takeoff_rounded, size: 36, color: Colors.white), + child: Center( + child: Icon( + isExclusive + ? Icons.workspace_premium_rounded + : isLatest + ? Icons.local_offer_rounded + : Icons.flight_takeoff_rounded, + size: 36, + color: isExclusive ? const Color(0xFFDCC58F) : Colors.white, + ), ), ), ); @@ -960,19 +1246,31 @@ class _DashboardScreenForDependetState extends State }, textInputAction: TextInputAction.search, textAlignVertical: TextAlignVertical.center, - style: const TextStyle(color: Color(0xFF202F31), fontSize: 13, fontWeight: FontWeight.w400), + style: const TextStyle( + color: Color(0xFF202F31), + fontSize: 13, + fontWeight: FontWeight.w400, + ), decoration: InputDecoration( isDense: true, contentPadding: EdgeInsets.zero, hintText: LocaleKeys.findAnEscapeOrAnExperience.tr(), - hintStyle: const TextStyle(color: Color(0xFF7A8180), fontSize: 13, fontWeight: FontWeight.w400), + hintStyle: const TextStyle( + color: Color(0xFF7A8180), + fontSize: 13, + fontWeight: FontWeight.w400, + ), border: InputBorder.none, ), ), ), ValueListenableBuilder( valueListenable: _searchController, - builder: (BuildContext context, TextEditingValue value, Widget? child) { + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { if (value.text.isEmpty) return const SizedBox.shrink(); return InkWell( borderRadius: BorderRadius.circular(16), @@ -1002,7 +1300,10 @@ class _DashboardScreenForDependetState extends State } Widget _buildSubcategoriesAndFilterRow(bool isAr) { - bool showSubcategoriesRow = currentCategoryId.isNotEmpty && currentCategoryId != "all" && (isSubcategoriesLoading || subcategoriesList.isNotEmpty); + bool showSubcategoriesRow = + currentCategoryId.isNotEmpty && + currentCategoryId != "all" && + (isSubcategoriesLoading || subcategoriesList.isNotEmpty); int activeFilterCount = _activeFilterCount(); return Padding( @@ -1018,13 +1319,27 @@ class _DashboardScreenForDependetState extends State borderRadius: BorderRadius.circular(6), onTap: _showFiltersBottomSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), alignment: Alignment.center, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: MyColors.lightGreyEAColor), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: MyColors.lightGreyEAColor, + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset("assets/icons/filter_icon.svg", width: 14, height: 14, colorFilter: const ColorFilter.mode(MyColors.black, BlendMode.srcIn)), + SvgPicture.asset( + "assets/icons/filter_icon.svg", + width: 14, + height: 14, + colorFilter: const ColorFilter.mode( + MyColors.black, + BlendMode.srcIn, + ), + ), const SizedBox(width: 5), LocaleKeys.filters.tr().toText12(color: MyColors.black), ], @@ -1039,14 +1354,25 @@ class _DashboardScreenForDependetState extends State width: 18, height: 18, alignment: Alignment.center, - decoration: const BoxDecoration(color: MyColors.darkIconColor, shape: BoxShape.circle), - child: '$activeFilterCount'.toText10(isBold: true, color: Colors.white), + decoration: const BoxDecoration( + color: MyColors.darkIconColor, + shape: BoxShape.circle, + ), + child: '$activeFilterCount'.toText10( + isBold: true, + color: Colors.white, + ), ), ), ], ), if (showSubcategoriesRow) ...[ - Container(margin: const EdgeInsets.symmetric(horizontal: 10), width: 1, height: 20, color: const Color(0xFFE2E8F0)), + Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + width: 1, + height: 20, + color: const Color(0xFFE2E8F0), + ), Expanded( child: isSubcategoriesLoading @@ -1057,34 +1383,76 @@ class _DashboardScreenForDependetState extends State scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), itemCount: 4, - separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8), - itemBuilder: (BuildContext _, int __) => Container(width: 70, height: 32, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 8), + itemBuilder: + (BuildContext _, int __) => Container( + width: 70, + height: 32, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + ), + ), ), ) : ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: subcategoriesList.length + 1, - separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8), + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 8), itemBuilder: (BuildContext context, int index) { bool isAllChip = index == 0; - WalaPlusCategoryModel? subcategory = isAllChip ? null : subcategoriesList[index - 1]; - bool isSelected = isAllChip ? _selectedSubcategoryId == null : _selectedSubcategoryId == subcategory!.id; - String label = isAllChip ? LocaleKeys.allCategories.tr() : (subcategory!.name ?? ""); + WalaPlusCategoryModel? subcategory = + isAllChip ? null : subcategoriesList[index - 1]; + bool isSelected = + isAllChip + ? _selectedSubcategoryId == null + : _selectedSubcategoryId == subcategory!.id; + String label = + isAllChip + ? LocaleKeys.allCategories.tr() + : (subcategory!.name ?? ""); return InkWell( borderRadius: BorderRadius.circular(6), onTap: () { setState(() { - _selectedSubcategoryId = isAllChip ? null : (isSelected ? null : subcategory!.id); + _selectedSubcategoryId = + isAllChip + ? null + : (isSelected + ? null + : subcategory!.id); }); - fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId); + fetchOffers( + categoryId: + _selectedSubcategoryId ?? + currentCategoryId, + ); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), alignment: Alignment.center, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor), - child: label.toText12(color: isSelected ? MyColors.white : MyColors.black), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: + isSelected + ? MyColors.darkIconColor + : MyColors.lightGreyEAColor, + ), + child: label.toText12( + color: + isSelected + ? MyColors.white + : MyColors.black, + ), ), ); }, @@ -1098,69 +1466,10 @@ class _DashboardScreenForDependetState extends State } void _showCitySelectionBottomSheet() { - List cities = [ - LocaleKeys.riyadh.tr(), - AppState().isArabic(context) ? "جدة" : "Jeddah", - AppState().isArabic(context) ? "الدمام" : "Dammam", - AppState().isArabic(context) ? "الخبر" : "Khobar", - AppState().isArabic(context) ? "مكة المكرمة" : "Mecca", - AppState().isArabic(context) ? "المدينة المنورة" : "Medina", - ]; - - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (BuildContext sheetContext) { - return Container( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2)), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)), - InkWell( - onTap: () => Navigator.pop(sheetContext), - borderRadius: BorderRadius.circular(16), - child: const Padding( - padding: EdgeInsets.all(4), - child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B)), - ), - ), - ], - ), - const SizedBox(height: 12), - ...cities.map((city) { - bool isSelected = selectedCity == city; - return ListTile( - title: city.toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)), - trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null, - onTap: () { - setState(() { - selectedCity = city; - }); - Navigator.pop(sheetContext); - }, - ); - }).toList(), - ], - ), - ), - ); - }, + showMazayaCitySelectionBottomSheet( + context, + selectedCity: selectedCity, + onCitySelected: (city) => setState(() => selectedCity = city), ); } @@ -1183,7 +1492,12 @@ class _DashboardScreenForDependetState extends State builder: (BuildContext sheetContext) { return StatefulBuilder( builder: (BuildContext context, StateSetter setSheetState) { - Widget buildOption(String label, bool isSelected, VoidCallback onTap, {IconData? icon}) { + Widget buildOption( + String label, + bool isSelected, + VoidCallback onTap, { + IconData? icon, + }) { return Expanded( child: InkWell( borderRadius: BorderRadius.circular(8), @@ -1193,15 +1507,44 @@ class _DashboardScreenForDependetState extends State alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: isSelected ? MyColors.lightGreyEAColor : Colors.white, - border: Border.all(color: isSelected ? MyColors.lightGreyEAColor : borderColor), + color: + isSelected ? MyColors.lightGreyEAColor : Colors.white, + border: Border.all( + color: + isSelected + ? MyColors.lightGreyEAColor + : borderColor, + ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (icon != null) ...[Icon(icon, size: 14, color: isSelected ? MyColors.black : const Color(0xFF64748B)), const SizedBox(width: 5)], - Flexible(child: label.toText13(isBold: true, color: isSelected ? MyColors.black : darkBg, maxlines: 1)), - if (isSelected) ...[const SizedBox(width: 5), const Icon(Icons.check, size: 14, color: MyColors.black)], + if (icon != null) ...[ + Icon( + icon, + size: 14, + color: + isSelected + ? MyColors.black + : const Color(0xFF64748B), + ), + const SizedBox(width: 5), + ], + Flexible( + child: label.toText13( + isBold: true, + color: isSelected ? MyColors.black : darkBg, + maxlines: 1, + ), + ), + if (isSelected) ...[ + const SizedBox(width: 5), + const Icon( + Icons.check, + size: 14, + color: MyColors.black, + ), + ], ], ), ), @@ -1212,7 +1555,11 @@ class _DashboardScreenForDependetState extends State Widget sectionHeader(String title, {String? hint}) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [title.toText14(isBold: true, color: darkBg), if (hint != null) hint.toText12(color: const Color(0xFF94A3B8))], + children: [ + title.toText14(isBold: true, color: darkBg), + if (hint != null) + hint.toText12(color: const Color(0xFF94A3B8)), + ], ); } @@ -1220,7 +1567,10 @@ class _DashboardScreenForDependetState extends State top: false, child: Container( padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -1230,13 +1580,19 @@ class _DashboardScreenForDependetState extends State width: 44, height: 4.5, margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5)), + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.filters.tr().toText20(isBold: true, color: darkBg), + LocaleKeys.filters.tr().toText20( + isBold: true, + color: darkBg, + ), InkWell( borderRadius: BorderRadius.circular(16), onTap: () => Navigator.pop(sheetContext), @@ -1244,8 +1600,15 @@ class _DashboardScreenForDependetState extends State width: 32, height: 32, alignment: Alignment.center, - decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFFF1F5F9)), - child: const Icon(Icons.close, size: 18, color: darkBg), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFF1F5F9), + ), + child: const Icon( + Icons.close, + size: 18, + color: darkBg, + ), ), ), ], @@ -1253,59 +1616,133 @@ class _DashboardScreenForDependetState extends State 20.height, // Availability - single select, drives the real offerType filter. - LocaleKeys.availability.tr().toText14(isBold: true, color: darkBg), + LocaleKeys.availability.tr().toText14( + isBold: true, + color: darkBg, + ), 10.height, Row( children: [ - buildOption(LocaleKeys.allCategories.tr(), draftAvailability == 'all', () => setSheetState(() => draftAvailability = 'all')), + buildOption( + LocaleKeys.allCategories.tr(), + draftAvailability == 'all', + () => setSheetState(() => draftAvailability = 'all'), + ), const SizedBox(width: 10), - buildOption(LocaleKeys.online.tr(), draftAvailability == 'online', () => setSheetState(() => draftAvailability = 'online')), + buildOption( + LocaleKeys.online.tr(), + draftAvailability == 'online', + () => + setSheetState(() => draftAvailability = 'online'), + ), const SizedBox(width: 10), - buildOption(LocaleKeys.inStore.tr(), draftAvailability == 'instore', () => setSheetState(() => draftAvailability = 'instore')), + buildOption( + LocaleKeys.inStore.tr(), + draftAvailability == 'instore', + () => setSheetState( + () => draftAvailability = 'instore', + ), + ), ], ), 20.height, // Sort by - single select, Nearest and Newest only. - LocaleKeys.sortBy.tr().toText14(isBold: true, color: darkBg), + LocaleKeys.sortBy.tr().toText14( + isBold: true, + color: darkBg, + ), 10.height, Row( children: [ - buildOption(LocaleKeys.nearest.tr(), draftSortBy == 'nearest', () => setSheetState(() => draftSortBy = 'nearest'), icon: Icons.location_on_outlined), + buildOption( + LocaleKeys.nearest.tr(), + draftSortBy == 'nearest', + () => setSheetState(() => draftSortBy = 'nearest'), + icon: Icons.location_on_outlined, + ), const SizedBox(width: 10), - buildOption(LocaleKeys.newest.tr(), draftSortBy == 'newest', () => setSheetState(() => draftSortBy = 'newest')), + buildOption( + LocaleKeys.newest.tr(), + draftSortBy == 'newest', + () => setSheetState(() => draftSortBy = 'newest'), + ), ], ), 20.height, // Distance - single select. - sectionHeader(LocaleKeys.distance.tr(), hint: LocaleKeys.fromYourCurrentLocation.tr()), + sectionHeader( + LocaleKeys.distance.tr(), + hint: LocaleKeys.fromYourCurrentLocation.tr(), + ), 10.height, Row( children: [ - buildOption(LocaleKeys.any.tr(), draftDistance == 'any', () => setSheetState(() => draftDistance = 'any')), + buildOption( + LocaleKeys.any.tr(), + draftDistance == 'any', + () => setSheetState(() => draftDistance = 'any'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.distanceKm.tr(args: ['5']), draftDistance == '5km', () => setSheetState(() => draftDistance = '5km')), + buildOption( + LocaleKeys.distanceKm.tr(args: ['5']), + draftDistance == '5km', + () => setSheetState(() => draftDistance = '5km'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.distanceKm.tr(args: ['10']), draftDistance == '10km', () => setSheetState(() => draftDistance = '10km')), + buildOption( + LocaleKeys.distanceKm.tr(args: ['10']), + draftDistance == '10km', + () => setSheetState(() => draftDistance = '10km'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.distanceKm.tr(args: ['25']), draftDistance == '25km', () => setSheetState(() => draftDistance = '25km')), + buildOption( + LocaleKeys.distanceKm.tr(args: ['25']), + draftDistance == '25km', + () => setSheetState(() => draftDistance = '25km'), + ), ], ), 20.height, // Minimum discount - single select. - LocaleKeys.minimumDiscount.tr().toText14(isBold: true, color: darkBg), + LocaleKeys.minimumDiscount.tr().toText14( + isBold: true, + color: darkBg, + ), 10.height, Row( children: [ - buildOption(LocaleKeys.any.tr(), draftMinDiscount == 'any', () => setSheetState(() => draftMinDiscount = 'any')), + buildOption( + LocaleKeys.any.tr(), + draftMinDiscount == 'any', + () => setSheetState(() => draftMinDiscount = 'any'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.discountPercentPlus.tr(args: ['10']), draftMinDiscount == '10', () => setSheetState(() => draftMinDiscount = '10')), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['10'], + ), + draftMinDiscount == '10', + () => setSheetState(() => draftMinDiscount = '10'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.discountPercentPlus.tr(args: ['20']), draftMinDiscount == '20', () => setSheetState(() => draftMinDiscount = '20')), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['20'], + ), + draftMinDiscount == '20', + () => setSheetState(() => draftMinDiscount = '20'), + ), const SizedBox(width: 8), - buildOption(LocaleKeys.discountPercentPlus.tr(args: ['30']), draftMinDiscount == '30', () => setSheetState(() => draftMinDiscount = '30')), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['30'], + ), + draftMinDiscount == '30', + () => setSheetState(() => draftMinDiscount = '30'), + ), ], ), 20.height, @@ -1331,7 +1768,12 @@ class _DashboardScreenForDependetState extends State _filterMinDiscount = draftMinDiscount; }); Navigator.pop(sheetContext); - fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId); + fetchOffers( + categoryId: + currentCategoryId.isEmpty + ? "all" + : currentCategoryId, + ); }, colors: const [Colors.white, Colors.white], border: Border.all(color: borderColor), @@ -1340,16 +1782,22 @@ class _DashboardScreenForDependetState extends State ), const SizedBox(width: 12), Expanded( - child: DefaultButton(LocaleKeys.applyFilters.tr(), () { - setState(() { - selectedTypeFilter = draftAvailability; - _filterSortBy = draftSortBy; - _filterDistance = draftDistance; - _filterMinDiscount = draftMinDiscount; - }); - Navigator.pop(sheetContext); - fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId); - }), + child: DefaultButton( + LocaleKeys.applyFilters.tr(), + () { + setState(() { + selectedTypeFilter = draftAvailability; + _filterSortBy = draftSortBy; + _filterDistance = draftDistance; + _filterMinDiscount = draftMinDiscount; + }); + Navigator.pop(sheetContext); + fetchOffers( + categoryId: + _selectedSubcategoryId ?? currentCategoryId, + ); + }, + ), ), ], ), @@ -1377,56 +1825,6 @@ class _DashboardScreenForDependetState extends State setState(() {}); } } - - Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) { - return WalaPlusRetailerCard( - item: item, - isAr: isAr, - onTap: () => _openRetailer(item), - onFavoriteTap: () => _toggleFavorite(item), - ); - } - - Widget _buildRetailersShimmer() { - return _buildRetailersShimmerWidget(isTablet); - } -} - -Widget _buildRetailersShimmerWidget(bool isTablet) { - return Shimmer.fromColors( - baseColor: const Color(0xFFE2E8F0), - highlightColor: const Color(0xFFF8FAFC), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16), - itemCount: 4, - itemBuilder: (_, __) { - return Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container(height: 120, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))), - const SizedBox(height: 8), - Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), - const SizedBox(height: 6), - Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), - const SizedBox(height: 6), - Row( - children: [ - Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), - const SizedBox(width: 5), - Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), - ], - ), - ], - ), - ); - }, - ), - ); } class WalaPlusRetailerCard extends StatelessWidget { @@ -1435,7 +1833,13 @@ class WalaPlusRetailerCard extends StatelessWidget { final VoidCallback onTap; final VoidCallback? onFavoriteTap; - const WalaPlusRetailerCard({Key? key, required this.item, required this.isAr, required this.onTap, this.onFavoriteTap}) : super(key: key); + const WalaPlusRetailerCard({ + Key? key, + required this.item, + required this.isAr, + required this.onTap, + this.onFavoriteTap, + }) : super(key: key); static String getDiscountBadge(WalaPlusRetailerModel item) { if (item.offerTitle != null) { @@ -1475,20 +1879,19 @@ class WalaPlusRetailerCard extends StatelessWidget { return Icons.tune_rounded; } - static Widget buildMerchantMIcon() { - return Container(color: const Color(0xFFED1C24), alignment: Alignment.center, child: "M".toText10(isBold: true, color: Colors.white)); - } - @override Widget build(BuildContext context) { - String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty) ? item.imageThumb : item.image; - String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) ? item.logoThumb : item.logo; + String? bannerUrl = + (item.imageThumb != null && item.imageThumb!.isNotEmpty) + ? item.imageThumb + : item.image; String discountText = getDiscountBadge(item); String offerSubtitle = ""; if (discountText.isNotEmpty) { - if (item.offerTitle != null && item.offerTitle!.toLowerCase().contains("off")) { + if (item.offerTitle != null && + item.offerTitle!.toLowerCase().contains("off")) { offerSubtitle = item.offerTitle!; } else { offerSubtitle = LocaleKeys.offDiscount.tr(args: [discountText]); @@ -1500,7 +1903,9 @@ class WalaPlusRetailerCard extends StatelessWidget { List offerTypes = item.normalizedTypes; String usageText = ""; if (item.displayUsage.isNotEmpty) { - usageText = item.displayUsage.replaceAll('usage:', LocaleKeys.usageLabel.tr()).replaceAll('Usage :', LocaleKeys.usageLabel.tr()); + usageText = item.displayUsage + .replaceAll('usage:', LocaleKeys.usageLabel.tr()) + .replaceAll('Usage :', LocaleKeys.usageLabel.tr()); } String cityText = item.getDisplayCity(isAr: isAr); if (cityText.trim().isEmpty) { @@ -1508,13 +1913,19 @@ class WalaPlusRetailerCard extends StatelessWidget { } return InkWell( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), onTap: onTap, child: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [BoxShadow(color: const Color(0xFF0D1720).withOpacity(0.0392), offset: const Offset(0, 5), blurRadius: 18, spreadRadius: 0)], + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 24, + spreadRadius: 0, + ), + ], ), padding: const EdgeInsets.all(10), child: SingleChildScrollView( @@ -1522,10 +1933,17 @@ class WalaPlusRetailerCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Figma redesign (node 927:496): shorter image, no bottom gradient/logo overlay + // (brand moved into the card body below), no discount ribbon - the percentage now + // lives inline in the teal offer subtitle text instead. Image height trimmed from + // 120->110 (not Figma's 82) and padding/gaps left untouched on purpose: this card's + // overall size is mainAxisExtent-driven (see the grid's gridDelegate) and was + // already tuned against real devices - copying Figma's absolute pixels here would + // reintroduce the overflow/empty-space problems that tuning fixed. ClipRRect( borderRadius: BorderRadius.circular(14), child: SizedBox( - height: 120, + height: 110, width: double.infinity, child: Stack( fit: StackFit.expand, @@ -1534,84 +1952,114 @@ class WalaPlusRetailerCard extends StatelessWidget { ? Image.network( bannerUrl, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)), + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF94A3B8), + size: 32, + ), + ), ) - : Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)), - Positioned( - bottom: 0, - left: 0, - right: 0, - height: 50, - child: Container( - decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(0.75), Colors.transparent])), - ), - ), - if (discountText.isNotEmpty) + : Container( + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF94A3B8), + size: 32, + ), + ), + // Exclusive offers (mapped from the legacy Offers & Discounts API) have no + // real WalaPlus retailer id, so there's nothing for add/removeWalaPlusFavorite + // to act on - hide the heart rather than wiring it to a call that can't work. + if (item.isExclusive != true) PositionedDirectional( - top: 44, - end: 0, - child: Container( - padding: const EdgeInsetsDirectional.only(start: 14, end: 10, top: 4, bottom: 4), - decoration: const BoxDecoration(color: Color(0xFFED1C24), borderRadius: BorderRadiusDirectional.only(topStart: Radius.circular(20), bottomStart: Radius.circular(20))), - child: discountText.toText14(isBold: true, color: Colors.white), + top: 7, + end: 7, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onFavoriteTap, + child: Container( + width: 27, + height: 27, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Icon( + item.isFavorite == true + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + size: 15, + color: + item.isFavorite == true + ? Colors.black + : const Color(0xFF1E293B), + ), + ), ), ), - PositionedDirectional( - top: 8, - end: 8, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: onFavoriteTap, + if (item.isExclusive == true) + PositionedDirectional( + bottom: 7, + start: 8, child: Container( - width: 30, - height: 30, + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 4, + ), decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 5, - offset: const Offset(0, 2), - ), - ], + color: const Color(0xFF143F36), + borderRadius: BorderRadius.circular(6), ), - alignment: Alignment.center, - child: Icon( - item.isFavorite == true ? Icons.favorite_rounded : Icons.favorite_border_rounded, - size: 17, - color: item.isFavorite == true ? const Color(0xFFE52332) : const Color(0xFF1E293B), + child: LocaleKeys.mazayaExclusive.tr().toText10( + color: const Color(0xFFF1EBDD), ), ), ), - ), - PositionedDirectional( - bottom: 8, - start: 8, - end: 8, - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4)), - clipBehavior: Clip.antiAlias, - child: (logoUrl != null && logoUrl.isNotEmpty) ? Image.network(logoUrl, fit: BoxFit.contain, errorBuilder: (_, __, ___) => buildMerchantMIcon()) : buildMerchantMIcon(), - ), - const SizedBox(width: 5), - Expanded(child: (item.name ?? "").toText11(isBold: true, maxLine: 1, color: Colors.white)), - ], - ), - ), ], ), ), ), const SizedBox(height: 8), - (item.name ?? "").toText14(isBold: true, maxlines: 1, overflow: TextOverflow.ellipsis, color: const Color(0xFF1E293B)), + // Brand name + "view details" affordance arrow, replacing the old image-overlay name. + Row( + children: [ + Expanded( + child: (item.name ?? "").toText13( + isBold: true, + maxlines: 1, + overflow: TextOverflow.ellipsis, + color: const Color(0xFF202F32), + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.north_east_rounded, + size: 16, + color: Color(0xFF147D6B), + ), + ], + ), const SizedBox(height: 2), - offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B)), + offerSubtitle.toText14( + maxlines: 1, + overflow: TextOverflow.ellipsis, + isBold: true, + color: const Color(0xFF147D6B), + ), const SizedBox(height: 6), + // Fully-rounded pill badges (no icon, no border) matching the new design - type + // chips in blue (In-store) / green (Online), city in grey. SizedBox( height: 28, child: ListView( @@ -1621,33 +2069,49 @@ class WalaPlusRetailerCard extends StatelessWidget { for (String offerType in offerTypes) ...[ InfoChipWidget( text: getTypeFilterTitle(offerType), - icon: getTypeFilterIcon(offerType), - backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF), - textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB), - borderColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF3B82F6), + backgroundColor: + offerType == 'online' + ? const Color(0xFFEAF3EE) + : const Color(0x14062CFF), + textColor: + offerType == 'online' + ? const Color(0xFF147D6B) + : const Color(0xFF2A56B6), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, horizontalPadding: 10, - verticalPadding: 5, - borderRadius: 12, - iconSize: 14, + verticalPadding: 6, + borderRadius: 99, ), const SizedBox(width: 5), ], if (cityText.trim().isNotEmpty) ...[ InfoChipWidget( text: cityText, - icon: Icons.location_on_outlined, - backgroundColor: const Color(0xFFF8FAFC), - textColor: const Color(0xFF475569), - borderColor: const Color(0xFFCBD5E1), + backgroundColor: const Color(0xFFEFF2EF), + textColor: const Color(0xFF464746), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, horizontalPadding: 10, - verticalPadding: 5, - borderRadius: 12, - iconSize: 14, + verticalPadding: 6, + borderRadius: 99, ), const SizedBox(width: 5), ], if (usageText.trim().isNotEmpty) - InfoChipWidget(text: usageText, backgroundColor: const Color(0xFFFEF3C7), textColor: const Color(0xFFB45309), horizontalPadding: 10, verticalPadding: 5, borderRadius: 12), + InfoChipWidget( + text: usageText, + backgroundColor: const Color(0xFFFEF3C7), + textColor: const Color(0xFFB45309), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, + horizontalPadding: 10, + verticalPadding: 6, + borderRadius: 99, + ), ], ), ), @@ -1661,13 +2125,17 @@ class WalaPlusRetailerCard extends StatelessWidget { class FavoriteMerchantsBottomSheet extends StatefulWidget { final List? categoriesList; - const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList}) : super(key: key); + + const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList}) + : super(key: key); @override - State createState() => _FavoriteMerchantsBottomSheetState(); + State createState() => + _FavoriteMerchantsBottomSheetState(); } -class _FavoriteMerchantsBottomSheetState extends State { +class _FavoriteMerchantsBottomSheetState + extends State { List favoriteRetailers = []; bool isLoading = true; bool isLoadingMore = false; @@ -1732,30 +2200,36 @@ class _FavoriteMerchantsBottomSheetState extends State[]; for (WalaPlusRetailerModel r in favoriteRetailers) { r.isFavorite = true; } currentPage = 1; totalPages = response?.pagesCount ?? 1; - hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false); + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); } catch (e) { debugPrint("Error fetching favorite retailers: $e"); favoriteRetailers = []; @@ -1778,25 +2252,30 @@ class _FavoriteMerchantsBottomSheetState extends State newRetailers = response?.retailers ?? []; + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: nextPage, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + + List newRetailers = + response?.retailers ?? []; if (newRetailers.isNotEmpty) { for (WalaPlusRetailerModel r in newRetailers) { r.isFavorite = true; @@ -1845,6 +2324,7 @@ class _FavoriteMerchantsBottomSheetState extends State r.id == item.id); } }); + Utils.showLoading(context); try { GenericResponseModel? response; @@ -1863,23 +2343,40 @@ class _FavoriteMerchantsBottomSheetState extends State( valueListenable: _searchController, - builder: (BuildContext context, TextEditingValue value, Widget? child) { - if (value.text.isEmpty) return const SizedBox.shrink(); + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { + if (value.text.isEmpty) + return const SizedBox.shrink(); return InkWell( - child: const Icon(Icons.clear, size: 18, color: Color(0xFF848486)), + child: const Icon( + Icons.clear, + size: 18, + color: Color(0xFF848486), + ), onTap: () { _searchController.clear(); _onSearchChanged(""); @@ -1990,9 +2547,16 @@ class _FavoriteMerchantsBottomSheetState extends State fetchFavorites(query: _searchController.text), + onRefresh: + () => fetchFavorites(query: _searchController.text), color: MyColors.textMixColor, - child: - isLoading - ? Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: _buildRetailersShimmerWidget(isTablet)) - : favoriteRetailers.isNotEmpty - ? SingleChildScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16), - itemCount: favoriteRetailers.length, - itemBuilder: (context, index) { - return WalaPlusRetailerCard( - item: favoriteRetailers[index], - isAr: isAr, - onTap: () => _openRetailer(favoriteRetailers[index]), - onFavoriteTap: () => _toggleFavorite(favoriteRetailers[index]), - ); - }, - ), - if (isLoadingMore) ...[ - 16.height, - Center( - child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0)), - ), - ], - const SizedBox(height: 20), - ], - ), - ) - : SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - child: Padding(padding: const EdgeInsets.only(top: 80), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))), - ), + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: favoriteRetailers, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + ), + const SizedBox(height: 20), + ], + ), + ), ), ), ], diff --git a/lib/ui/mazaya/saved_retailers_screen.dart b/lib/ui/mazaya/saved_retailers_screen.dart new file mode 100644 index 0000000..4e2bf0e --- /dev/null +++ b/lib/ui/mazaya/saved_retailers_screen.dart @@ -0,0 +1,386 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.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/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; + +// Mazaya Saved tab - full-screen version of what was FavoriteMerchantsBottomSheet (a modal +// bottom sheet). Same fetch/search/pagination/toggle-favorite logic, now its own pushed route +// with the shared MazayaBottomNav instead of a dismissible sheet - matches Discover's "own +// route, fetches fresh on mount" pattern. +class SavedRetailersScreen extends StatefulWidget { + const SavedRetailersScreen({Key? key}) : super(key: key); + + @override + State createState() => _SavedRetailersScreenState(); +} + +class _SavedRetailersScreenState extends State { + List favoriteRetailers = []; + bool isLoading = true; + bool isLoadingMore = false; + bool hasMore = true; + bool _isFetchingPage = false; + int currentPage = 1; + int totalPages = 1; + final int pageSize = 10; + String searchQuery = ""; + Timer? _debounceTimer; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) fetchFavorites(); + }); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + double maxScroll = _scrollController.position.maxScrollExtent; + double currentScroll = _scrollController.position.pixels; + if (currentScroll >= maxScroll - 35) { + if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { + fetchNextPage(); + } + } + } + + String? _getUserReferenceForFavorites() { + String ref = OffersAndDiscountsApiClient().resolveUserReference(); + if (ref.trim().isNotEmpty) return ref.trim(); + String? userName = AppState().getUserName; + if (userName != null && userName.trim().isNotEmpty) return userName.trim(); + return null; + } + + Future fetchFavorites({String? query}) async { + if (query != null) searchQuery = query; + setState(() { + isLoading = true; + currentPage = 1; + hasMore = true; + }); + try { + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: 1, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + favoriteRetailers = response?.retailers ?? []; + for (WalaPlusRetailerModel r in favoriteRetailers) { + r.isFavorite = true; + } + currentPage = 1; + totalPages = response?.pagesCount ?? 1; + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); + } catch (e) { + debugPrint("MazayaSavedScreen: error fetching favorites: $e"); + favoriteRetailers = []; + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + Future fetchNextPage() async { + if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return; + _isFetchingPage = true; + setState(() => isLoadingMore = true); + try { + int nextPage = currentPage + 1; + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: nextPage, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + List newRetailers = + response?.retailers ?? []; + if (newRetailers.isNotEmpty) { + for (WalaPlusRetailerModel r in newRetailers) { + r.isFavorite = true; + } + currentPage = nextPage; + totalPages = response?.pagesCount ?? totalPages; + hasMore = currentPage < totalPages; + favoriteRetailers.addAll(newRetailers); + } else { + hasMore = false; + } + } catch (e) { + debugPrint("MazayaSavedScreen: error fetching next page: $e"); + } finally { + _isFetchingPage = false; + if (mounted) setState(() => isLoadingMore = false); + } + } + + void _onSearchChanged(String query) { + _debounceTimer?.cancel(); + searchQuery = query; + if (query.trim().isEmpty) { + fetchFavorites(query: ""); + } else { + _debounceTimer = Timer(const Duration(milliseconds: 600), () { + fetchFavorites(query: query.trim()); + }); + } + } + + Future _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? true; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + if (!targetFav) { + favoriteRetailers.removeWhere( + (WalaPlusRetailerModel r) => r.id == item.id, + ); + } + }); + Utils.showLoading(context); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + Utils.hideLoading(context); + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg = + isAr + ? (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr()) + : (response.wpFavoritesAddRemoveMessage ?? + (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr())); + Utils.showToast(msg, isSuccess: true); + } else { + Utils.showToast( + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(), + ); + await fetchFavorites(query: _searchController.text); + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("MazayaSavedScreen: error toggling favorite: $e"); + Utils.showToast(LocaleKeys.favoriteError.tr()); + await fetchFavorites(query: _searchController.text); + } + } + + void _openRetailer(WalaPlusRetailerModel item) async { + await Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + if (mounted) { + await fetchFavorites(query: _searchController.text); + } + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.favoriteOffers.tr(), + showHomeButton: false, + ), + bottomNavigationBar: const MazayaBottomNav(currentTab: MazayaTab.saved), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + width: double.infinity, + height: 70, + padding: const EdgeInsets.only( + top: 14, + right: 16, + bottom: 14, + left: 16, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0), + boxShadow: [ + BoxShadow( + color: const Color(0xFF000000).withOpacity(0.0314), + offset: const Offset(0, 2), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.search.tr().toText12( + isBold: true, + color: const Color(0xFF2A333C), + ), + const SizedBox(height: 2), + TextField( + controller: _searchController, + onChanged: _onSearchChanged, + onSubmitted: (String val) { + _debounceTimer?.cancel(); + fetchFavorites(query: val.trim()); + }, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.searchItems.tr(), + hintStyle: const TextStyle( + color: Color(0xFFACAFB1), + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.2, + ), + border: InputBorder.none, + suffixIcon: ValueListenableBuilder( + valueListenable: _searchController, + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { + if (value.text.isEmpty) + return const SizedBox.shrink(); + return InkWell( + child: const Icon( + Icons.clear, + size: 18, + color: Color(0xFF848486), + ), + onTap: () { + _searchController.clear(); + _onSearchChanged(""); + }, + ); + }, + ), + suffixIconConstraints: const BoxConstraints( + maxWidth: 24, + maxHeight: 28, + ), + ), + style: const TextStyle( + color: Color(0xFF2A333C), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + Expanded( + child: RefreshIndicator( + onRefresh: () => fetchFavorites(query: _searchController.text), + color: MyColors.textMixColor, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: favoriteRetailers, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart b/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart new file mode 100644 index 0000000..de6ff79 --- /dev/null +++ b/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart @@ -0,0 +1,85 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/ui/mazaya/mazaya_discover_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/family_members.dart'; + +enum MazayaTab { discover, saved, myFamily } + +// Shared bottom nav for the Mazaya module (Discover / Saved / My Family). Each tab is its own +// screen, swapped in with zero-duration transition (see _onTabTapped) so switching tabs feels +// instant rather than like a new screen opening - not an IndexedStack shell, since each tab still +// needs to reload its own data fresh on every switch, not preserve state across tabs. +class MazayaBottomNav extends StatelessWidget { + final MazayaTab currentTab; + + const MazayaBottomNav({Key? key, required this.currentTab}) : super(key: key); + + static const Color _activeColor = Color(0xFF147D6B); + static const Color _inactiveColor = Color(0xFF78817F); + + void _onTabTapped(BuildContext context, MazayaTab tab) { + if (tab == currentTab) return; + Widget targetScreen; + RouteSettings? settings; + switch (tab) { + case MazayaTab.discover: + targetScreen = const MazayaDiscoverScreen(); + break; + case MazayaTab.saved: + targetScreen = const SavedRetailersScreen(); + break; + case MazayaTab.myFamily: + targetScreen = const FamilyMembers(); + settings = const RouteSettings(arguments: {'fromMazaya': true}); + break; + } + Navigator.pushReplacement( + context, + PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) => targetScreen, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + } + + Widget _buildItem(BuildContext context, {required MazayaTab tab, required String svgAsset, required String label}) { + bool isActive = tab == currentTab; + return Expanded( + child: InkWell( + onTap: () => _onTabTapped(context, tab), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(svgAsset, width: 22, height: 22, colorFilter: ColorFilter.mode(isActive ? _activeColor : _inactiveColor, BlendMode.srcIn)), + const SizedBox(height: 5), + label.toText11(weight: isActive ? FontWeight.w600 : FontWeight.w400, color: isActive ? _activeColor : _inactiveColor), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + decoration: const BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Color(0xFFE8EAE4)))), + padding: const EdgeInsets.only(top: 13, bottom: 9), + child: Row( + children: [ + _buildItem(context, tab: MazayaTab.discover, svgAsset: "assets/icons/mazaya_compass.svg", label: LocaleKeys.discover.tr()), + _buildItem(context, tab: MazayaTab.saved, svgAsset: "assets/icons/mazaya_heart_outline.svg", label: LocaleKeys.saved.tr()), + _buildItem(context, tab: MazayaTab.myFamily, svgAsset: "assets/icons/mazaya_user_round.svg", label: LocaleKeys.myFamily.tr()), + ], + ), + ), + ); + } +} diff --git a/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart new file mode 100644 index 0000000..1afe71a --- /dev/null +++ b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart @@ -0,0 +1,97 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/main.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart'; +import 'package:shimmer/shimmer.dart'; + +class WalaPlusRetailerGrid extends StatelessWidget { + final bool isLoading; + final List retailers; + final bool isAr; + final void Function(WalaPlusRetailerModel item) onTapRetailer; + final void Function(WalaPlusRetailerModel item) onFavoriteTap; + final bool isLoadingMore; + final int shimmerItemCount; + + const WalaPlusRetailerGrid({ + Key? key, + required this.isLoading, + required this.retailers, + required this.isAr, + required this.onTapRetailer, + required this.onFavoriteTap, + this.isLoadingMore = false, + this.shimmerItemCount = 4, + }) : super(key: key); + + SliverGridDelegateWithFixedCrossAxisCount get _gridDelegate => + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16); + + @override + Widget build(BuildContext context) { + if (isLoading) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: _gridDelegate, + itemCount: shimmerItemCount, + itemBuilder: (BuildContext _, int __) => _buildShimmerCard(), + ), + ); + } + + if (retailers.isEmpty) { + return Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))); + } + + return Column( + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: _gridDelegate, + itemCount: retailers.length, + itemBuilder: (BuildContext context, int index) { + return WalaPlusRetailerCard(item: retailers[index], isAr: isAr, onTap: () => onTapRetailer(retailers[index]), onFavoriteTap: () => onFavoriteTap(retailers[index])); + }, + ), + if (isLoadingMore) ...[ + 16.height, + Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), + ], + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 120, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))), + const SizedBox(height: 8), + Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + const SizedBox(height: 6), + Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + const SizedBox(height: 6), + Row( + children: [ + Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), + const SizedBox(width: 5), + Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/profile/add_dependent_verify_screen.dart b/lib/ui/profile/add_dependent_verify_screen.dart index 9860841..8fb72a3 100644 --- a/lib/ui/profile/add_dependent_verify_screen.dart +++ b/lib/ui/profile/add_dependent_verify_screen.dart @@ -1858,7 +1858,7 @@ class _AddDependentVerifyScreenState extends State { onHomeTapped: () { Navigator.pushNamedAndRemoveUntil( context, - ApiConsts.ISDependetLogin ? AppRoutes.dashboardScreenForDependet : AppRoutes.dashboard, + ApiConsts.ISDependetLogin ? AppRoutes.mazayaDiscover : AppRoutes.dashboard, (Route route) => false, ); }, diff --git a/lib/ui/profile/dependent_details_screen.dart b/lib/ui/profile/dependent_details_screen.dart index ea76079..8b7d150 100644 --- a/lib/ui/profile/dependent_details_screen.dart +++ b/lib/ui/profile/dependent_details_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/api/profile_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; @@ -52,13 +53,22 @@ class _ShieldIconPainter extends CustomPainter { checkPath.lineTo(w * 0.66, h * 0.39); canvas.drawPath(checkPath, paint); } else { - canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint); - canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint); + canvas.drawLine( + Offset(w * 0.38, h * 0.38), + Offset(w * 0.62, h * 0.62), + paint, + ); + canvas.drawLine( + Offset(w * 0.62, h * 0.38), + Offset(w * 0.38, h * 0.62), + paint, + ); } } @override - bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted; + bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => + color != oldDelegate.color || isGranted != oldDelegate.isGranted; } class DependentDetailsScreen extends StatefulWidget { @@ -71,7 +81,8 @@ class DependentDetailsScreen extends StatefulWidget { // equivalent local state directly instead of needing this. final bool? recentAction; - const DependentDetailsScreen({Key? key, this.contact, this.recentAction}) : super(key: key); + const DependentDetailsScreen({Key? key, this.contact, this.recentAction}) + : super(key: key); @override _DependentDetailsScreenState createState() => _DependentDetailsScreenState(); @@ -107,7 +118,8 @@ class _DependentDetailsScreenState extends State { bool get _isAccessAllowed { if (_contact == null) return false; - String? flag = _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString(); + String? flag = + _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString(); return flag?.trim().toUpperCase() == 'Y'; } @@ -118,17 +130,29 @@ class _DependentDetailsScreenState extends State { String _getInitials(String? name) { if (name == null || name.trim().isEmpty) return "D"; - List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + List parts = + name + .trim() + .split(RegExp(r'\s+')) + .where((String p) => p.isNotEmpty) + .toList(); if (parts.isEmpty) return "D"; if (parts.length == 1) { - return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); + return parts.first.length >= 2 + ? parts.first.substring(0, 2).toUpperCase() + : parts.first.toUpperCase(); } return "${parts.first[0]}${parts.last[0]}".toUpperCase(); } String _getFirstName(String? name) { if (name == null || name.trim().isEmpty) return ""; - List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + List parts = + name + .trim() + .split(RegExp(r'\s+')) + .where((String p) => p.isNotEmpty) + .toList(); if (parts.isEmpty) return ""; return parts.first; } @@ -142,7 +166,10 @@ class _DependentDetailsScreenState extends State { if (match != null) { try { int millis = int.parse(match.group(1)!); - DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); + DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch( + millis, + isUtc: true, + ); return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); } catch (_) {} } @@ -150,7 +177,10 @@ class _DependentDetailsScreenState extends State { if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) { try { int millis = int.parse(s); - DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); + DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch( + millis, + isUtc: true, + ); return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); } catch (_) {} } @@ -161,11 +191,28 @@ class _DependentDetailsScreenState extends State { // DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip // that clock-time suffix first, since none of the date-only patterns below can match it // with it still attached (parseStrict requires the whole string to match the pattern). - String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim(); + String dateOnly = + s + .replaceFirst( + RegExp( + r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', + caseSensitive: false, + ), + '', + ) + .trim(); // MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an // ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June. - List formats = ['yyyy-MM-dd', 'MM/dd/yyyy', 'dd/MM/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy']; + List formats = [ + 'yyyy-MM-dd', + 'MM/dd/yyyy', + 'dd/MM/yyyy', + 'dd-MM-yyyy', + 'yyyy/MM/dd', + 'dd-MMM-yyyy', + 'dd MMM yyyy', + ]; for (String fmt in formats) { try { DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly); @@ -205,9 +252,20 @@ class _DependentDetailsScreenState extends State { ConfirmBottomSheet.show( context, - title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), - message: isAllowed ? LocaleKeys.confirmRevokeMazayaAccess.tr(args: [dependentName]) : LocaleKeys.confirmGrantMazayaAccess.tr(args: [dependentName]), - icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, + title: + isAllowed + ? LocaleKeys.revokeMazayaAccess.tr() + : LocaleKeys.allowMazayaAccess.tr(), + message: + isAllowed + ? LocaleKeys.confirmRevokeMazayaAccess.tr( + args: [dependentName], + ) + : LocaleKeys.confirmGrantMazayaAccess.tr( + args: [dependentName], + ), + icon: + isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), confirmText: LocaleKeys.yes.tr(), cancelText: LocaleKeys.no.tr(), @@ -271,7 +329,10 @@ class _DependentDetailsScreenState extends State { // carry identical content, so this is the single source of truth for it. String _buildMazayaInviteMessage(String linkToShare) { String? nationalId = _contact?.nATIONALIDENTIFIER?.trim(); - String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}"; + String nationalIdLine = + (nationalId == null || nationalId.isEmpty) + ? LocaleKeys.nationalIdOrIqamaFallback.tr() + : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}"; return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}"; } @@ -291,11 +352,16 @@ class _DependentDetailsScreenState extends State { // (including WhatsApp) just attaches the file with nothing to drop. Future _shareMazayaAccessGuide(bool isAr) async { try { - ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr)); + ByteData bytes = await rootBundle.load( + MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr), + ); Directory tempDir = await getTemporaryDirectory(); - String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; + String localFileName = + isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; File pdfFile = File('${tempDir.path}/$localFileName'); - await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); + await pdfFile.writeAsBytes( + bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes), + ); await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)])); } catch (_) {} @@ -307,7 +373,11 @@ class _DependentDetailsScreenState extends State { String initials = _getInitials(_contact?.cONTACTNAME); String fullName = _contact?.cONTACTNAME ?? ""; String firstName = _getFirstName(_contact?.cONTACTNAME); - String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty) ? _contact!.rELATIONSHIP!.trim() : (_contact?.cONTACTTYPE?.trim() ?? ""); + String relation = + (_contact?.rELATIONSHIP != null && + _contact!.rELATIONSHIP!.trim().isNotEmpty) + ? _contact!.rELATIONSHIP!.trim() + : (_contact?.cONTACTTYPE?.trim() ?? ""); String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER); String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH); bool isAllowed = _isAccessAllowed; @@ -328,15 +398,39 @@ class _DependentDetailsScreenState extends State { InkWell( borderRadius: BorderRadius.circular(20), onTap: () => Navigator.pop(context), - child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), + ), ), const SizedBox(width: 8), - LocaleKeys.dependentDetails.tr().toText20(color: const Color(0xFF1E293B)), + LocaleKeys.dependentDetails.tr().toText20( + color: const Color(0xFF1E293B), + ), const Spacer(), InkWell( borderRadius: BorderRadius.circular(20), - onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), - child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))), + onTap: + () => Navigator.popUntil( + context, + ModalRoute.withName( + ApiConsts.isDependetLogin + ? AppRoutes.mazayaDiscover + : AppRoutes.dashboard, + ), + ), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.home_outlined, + size: 24, + color: Color(0xFF1E293B), + ), + ), ), ], ), @@ -360,21 +454,39 @@ class _DependentDetailsScreenState extends State { child: Container( width: 72, height: 72, - decoration: BoxDecoration(color: _recentAction! ? const Color(0xFFE6F7F2) : const Color(0xFFFEECEB), shape: BoxShape.circle), + decoration: BoxDecoration( + color: + _recentAction! + ? const Color(0xFFE6F7F2) + : const Color(0xFFFEECEB), + shape: BoxShape.circle, + ), child: Center( child: CustomPaint( size: const Size(36, 36), - painter: _ShieldIconPainter(color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24), isGranted: _recentAction!), + painter: _ShieldIconPainter( + color: + _recentAction! + ? const Color(0xFF168A89) + : const Color(0xFFED1C24), + isGranted: _recentAction!, + ), ), ), ), ), const SizedBox(height: 10), Center( - child: (_recentAction! ? LocaleKeys.mazayaAccessGranted.tr() : LocaleKeys.mazayaAccessRevoked.tr()).toText16( - weight: FontWeight.w500, - color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24), - ), + child: (_recentAction! + ? LocaleKeys.mazayaAccessGranted.tr() + : LocaleKeys.mazayaAccessRevoked.tr()) + .toText16( + weight: FontWeight.w500, + color: + _recentAction! + ? const Color(0xFF168A89) + : const Color(0xFFED1C24), + ), ), const SizedBox(height: 16), ], @@ -383,7 +495,14 @@ class _DependentDetailsScreenState extends State { Container( width: double.infinity, padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: const Color(0xFFE8F7F2), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2)), + decoration: BoxDecoration( + color: const Color(0xFFE8F7F2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFBCE6D7), + width: 1.2, + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -391,8 +510,15 @@ class _DependentDetailsScreenState extends State { Container( width: 52, height: 52, - decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Center(child: initials.toText17(color: const Color(0xFF168A89))), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Center( + child: initials.toText17( + color: const Color(0xFF168A89), + ), + ), ), const SizedBox(width: 14), @@ -402,14 +528,32 @@ class _DependentDetailsScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - fullName.toText16(maxlines: 2, weight: FontWeight.w500, color: const Color(0xFF1E293B)), - if (relation.isNotEmpty) ...[const SizedBox(height: 3), relation.toText12(maxLine: 1, weight: FontWeight.w500, color: const Color(0xFF168A89))], + fullName.toText16( + maxlines: 2, + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + if (relation.isNotEmpty) ...[ + const SizedBox(height: 3), + relation.toText12( + maxLine: 1, + weight: FontWeight.w500, + color: const Color(0xFF168A89), + ), + ], ], ), ), // Verified badge on right - only when NHIC has actually verified this contact - if (_isNhicVerified) ...[const SizedBox(width: 8), const Icon(Icons.verified_rounded, color: Color(0xFF059669), size: 24)], + if (_isNhicVerified) ...[ + const SizedBox(width: 8), + const Icon( + Icons.verified_rounded, + color: Color(0xFF059669), + size: 24, + ), + ], ], ), ), @@ -424,33 +568,62 @@ class _DependentDetailsScreenState extends State { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF1F5F9)), - boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))], + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), - if (_isNhicVerified) LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)), + LocaleKeys.keyDetails.tr().toText16( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + if (_isNhicVerified) + LocaleKeys.verified.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF059669), + ), ], ), const SizedBox(height: 14), - const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.nationalId.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), - formattedId.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.nationalId.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + formattedId.toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.dateOfBirth.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), - formattedDob.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.dateOfBirth.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + formattedDob.toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), ], ), ], @@ -460,9 +633,15 @@ class _DependentDetailsScreenState extends State { const SizedBox(height: 24), // Services & actions section header - LocaleKeys.servicesAndActions.tr().toText17(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.servicesAndActions.tr().toText17( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), const SizedBox(height: 4), - LocaleKeys.startDependentFlowsDesc.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), + LocaleKeys.startDependentFlowsDesc.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), const SizedBox(height: 12), @@ -474,7 +653,13 @@ class _DependentDetailsScreenState extends State { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF1F5F9)), - boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))], + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -483,29 +668,66 @@ class _DependentDetailsScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.mazayaAccess.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.mazayaAccess.tr().toText16( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), - decoration: BoxDecoration(color: isAllowed ? const Color(0xFFECFDF5) : const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(12)), - child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.accessOff.tr()).toText12( - weight: FontWeight.w500, - color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 3, ), + decoration: BoxDecoration( + color: + isAllowed + ? const Color(0xFFECFDF5) + : const Color(0xFFFEE2E2), + borderRadius: BorderRadius.circular(12), + ), + child: (isAllowed + ? LocaleKeys.active.tr() + : LocaleKeys.accessOff.tr()) + .toText12( + weight: FontWeight.w500, + color: + isAllowed + ? const Color(0xFF059669) + : const Color(0xFFEF4444), + ), ), ], ), const SizedBox(height: 6), - LocaleKeys.allowDependentMazayaOffersDesc.tr(args: [firstName.isNotEmpty ? firstName : fullName]).toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), + LocaleKeys.allowDependentMazayaOffersDesc + .tr( + args: [ + firstName.isNotEmpty ? firstName : fullName, + ], + ) + .toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), const SizedBox(height: 16), - const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), const SizedBox(height: 14), // Login method row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.loginMethod.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), - LocaleKeys.nationalIdAndDob.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.loginMethod.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + LocaleKeys.nationalIdAndDob.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), ], ), const SizedBox(height: 12), @@ -514,8 +736,14 @@ class _DependentDetailsScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.verification.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), - LocaleKeys.sponsorOtp.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), + LocaleKeys.verification.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + LocaleKeys.sponsorOtp.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), ], ), @@ -529,19 +757,46 @@ class _DependentDetailsScreenState extends State { height: 48, width: double.infinity, decoration: BoxDecoration( - color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6), + color: + isAllowed + ? const Color(0xFFFEF2F2) + : const Color(0xFF22B0A6), borderRadius: BorderRadius.circular(12), - border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null, + border: + isAllowed + ? Border.all( + color: const Color(0xFFFECACA), + width: 1.2, + ) + : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox(width: 18, height: 18, child: CustomPaint(painter: _ShieldIconPainter(color: isAllowed ? const Color(0xFFDC2626) : Colors.white, isGranted: !isAllowed))), - const SizedBox(width: 8), - (isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr()).toText16( - weight: FontWeight.w500, - color: isAllowed ? const Color(0xFFDC2626) : Colors.white, + SizedBox( + width: 18, + height: 18, + child: CustomPaint( + painter: _ShieldIconPainter( + color: + isAllowed + ? const Color(0xFFDC2626) + : Colors.white, + isGranted: !isAllowed, + ), + ), ), + const SizedBox(width: 8), + (isAllowed + ? LocaleKeys.revokeMazayaAccess.tr() + : LocaleKeys.allowMazayaAccess.tr()) + .toText16( + weight: FontWeight.w500, + color: + isAllowed + ? const Color(0xFFDC2626) + : Colors.white, + ), ], ), ), @@ -549,7 +804,15 @@ class _DependentDetailsScreenState extends State { if (isAllowed) ...[ const SizedBox(height: 12), - Center(child: LocaleKeys.revokingPreventsAccessDesc.tr().toText11(weight: FontWeight.w400, color: const Color(0xFF64748B), isCenter: true)), + Center( + child: LocaleKeys.revokingPreventsAccessDesc + .tr() + .toText11( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + isCenter: true, + ), + ), ], ], ), @@ -570,13 +833,24 @@ class _DependentDetailsScreenState extends State { child: DefaultButton( LocaleKeys.shareAccessGuide.tr(), () => _shareMazayaAccessGuide(isAr), - colors: const [Color(0xFFE8F7F2), Color(0xFFE8F7F2)], - border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2), + colors: const [ + Color(0xFFE8F7F2), + Color(0xFFE8F7F2), + ], + border: Border.all( + color: const Color(0xFFBCE6D7), + width: 1.2, + ), textColor: const Color(0xFF168A89), ), ), const SizedBox(width: 12), - Expanded(child: DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInviteLink(isAr, linkToShare))), + Expanded( + child: DefaultButton( + LocaleKeys.shareDownloadLink.tr(), + () => _shareMazayaInviteLink(isAr, linkToShare), + ), + ), ], ).insideContainer, ], diff --git a/lib/ui/profile/family_members.dart b/lib/ui/profile/family_members.dart index ed90415..0ae9bab 100644 --- a/lib/ui/profile/family_members.dart +++ b/lib/ui/profile/family_members.dart @@ -1,5 +1,4 @@ import 'dart:developer'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -13,6 +12,7 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/dashboard/menu_entries.dart'; import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; import 'package:mohem_flutter_app/ui/profile/dependent_details_screen.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; @@ -191,6 +191,9 @@ class _FamilyMembersState extends State { return Scaffold( backgroundColor: const Color(0xFFF8FAFC), + // Only shown when reached via the Mazaya "My Family" tab - this screen is also opened from + // the regular profile flow (fromMazaya == false), where the Mazaya bottom nav doesn't apply. + bottomNavigationBar: fromMazaya ? const MazayaBottomNav(currentTab: MazayaTab.myFamily) : null, appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: SafeArea( @@ -206,13 +209,22 @@ class _FamilyMembersState extends State { child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), ), const SizedBox(width: 8), - (fromMazaya ? LocaleKeys.myDependents.tr() : LocaleKeys.profile_familyDetails.tr()).toText20(isBold: true, color: const Color(0xFF1E293B)), + // Same title style as the other Mazaya tabs' headers (Discover/Saved use + // AppBarWidget's title.toText24(isBold: true, color: MyColors.darkTextColor)) - + // matched here since this screen builds its own header instead of AppBarWidget. + fromMazaya + ? LocaleKeys.myFamily.tr().toText24(isBold: true, color: MyColors.darkTextColor) + : LocaleKeys.profile_familyDetails.tr().toText20(isBold: true, color: const Color(0xFF1E293B)), const Spacer(), - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), - child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))), - ), + // Home button hidden in the Mazaya flow too, matching AppBarWidget's + // showHomeButton: false on the other tabs - MazayaBottomNav's own Discover tab + // already serves as "home" for this module. + if (!fromMazaya) + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))), + ), ], ), ), @@ -302,7 +314,7 @@ class _FamilyMembersState extends State { } // Same Shimmer.fromColors skeleton-card convention used elsewhere in the app (e.g. - // dashboard_screen_for_dependet.dart's retailer/category shimmers), shaped like the + // retailers_list_screen.dart's retailer/category shimmers), shaped like the // real _buildDependentCard layout so the loading state doesn't jump around once it loads. Widget _buildDependentsShimmer() { return Shimmer.fromColors( diff --git a/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart b/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart index dbc2366..407506f 100644 --- a/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart +++ b/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart @@ -61,62 +61,58 @@ class _OffersAndDiscountsHomeState extends State { LocaleKeys.browseCategories.tr().toText17().paddingOnly(left: 21, right: 21, top: 21), SizedBox( height: 110.0, - child: getCategoriesList.isNotEmpty - ? ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(left: 21, right: 21, top: 13, bottom: 13), - scrollDirection: Axis.horizontal, - itemBuilder: (cxt, index) { - return AspectRatio( - aspectRatio: 1 / 1, - child: InkWell( - onTap: () { - setState(() { - currentCategoryID = getCategoriesList[index].id!; - getCategoryOffersListAPI(); - // getItemsForSaleList.clear(); - // currentPageNo = 1; - // getItemsForSale(currentPageNo, currentCategoryID); - }); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.string( - getCategoriesList[index].content!, - fit: BoxFit.contain, - width: 25, - height: 25, - ), - currentCategoryID == getCategoriesList[index].id ? const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0) : Container(), - ], - ).expanded, - AppState().isArabic(context) ? getCategoriesList[index].categoryNameAr!.toText10() : getCategoriesList[index].categoryNameEn!.toText10() - ], - ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), - ), - ); - }, - separatorBuilder: (cxt, index) => 12.width, - itemCount: getCategoriesList.length, - ) - : Container(), + child: + getCategoriesList.isNotEmpty + ? ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13, bottom: 13), + scrollDirection: Axis.horizontal, + itemBuilder: (cxt, index) { + return AspectRatio( + aspectRatio: 1 / 1, + child: InkWell( + onTap: () { + setState(() { + currentCategoryID = getCategoriesList[index].id!; + getCategoryOffersListAPI(); + // getItemsForSaleList.clear(); + // currentPageNo = 1; + // getItemsForSale(currentPageNo, currentCategoryID); + }); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.string(getCategoriesList[index].content!, fit: BoxFit.contain, width: 25, height: 25), + currentCategoryID == getCategoriesList[index].id ? const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0) : Container(), + ], + ).expanded, + AppState().isArabic(context) ? getCategoriesList[index].categoryNameAr!.toText10() : getCategoriesList[index].categoryNameEn!.toText10(), + ], + ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), + ), + ); + }, + separatorBuilder: (cxt, index) => 12.width, + itemCount: getCategoriesList.length, + ) + : Container(), ), getOffersList.isNotEmpty ? GridView( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 162 / 266, crossAxisSpacing: 12, mainAxisSpacing: 12), - padding: const EdgeInsets.only(left: 21, right: 21, bottom: 21, top: 21), - shrinkWrap: true, - primary: false, - physics: const ScrollPhysics(), - children: getItemsForSaleWidgets(), - ) + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 162 / 266, crossAxisSpacing: 12, mainAxisSpacing: 12), + padding: const EdgeInsets.only(left: 21, right: 21, bottom: 21, top: 21), + shrinkWrap: true, + primary: false, + physics: const ScrollPhysics(), + children: getItemsForSaleWidgets(), + ) : Utils.getNoDataWidget(context).paddingOnly(top: 50), ], ), @@ -144,13 +140,7 @@ class _OffersAndDiscountsHomeState extends State { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: const Color(0xff000000).withOpacity(.05), - blurRadius: 26, - offset: const Offset(0, -3), - ), - ], + boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 26, offset: const Offset(0, -3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -159,16 +149,7 @@ class _OffersAndDiscountsHomeState extends State { Hero( tag: "ItemImage" + getOffersList.offersDiscountId.toString()!, transitionOnUserGestures: true, - child: AspectRatio( - aspectRatio: 118 / 127, - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - getOffersList.bannerImage ?? "", - fit: BoxFit.contain, - ), - ), - ), + child: AspectRatio(aspectRatio: 118 / 127, child: ClipRRect(borderRadius: BorderRadius.circular(6), child: Image.network(getOffersList.bannerImage ?? "", fit: BoxFit.contain))), ), AppState().isArabic(context) ? getOffersList.titleAr!.toText16(isBold: true, color: const Color(0xff2B353E), maxlines: 1) @@ -183,10 +164,7 @@ class _OffersAndDiscountsHomeState extends State { // 8.height, getOffersList.discountDescription!.toText14(isBold: true, maxlines: 1), 20.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [checkDate(getOffersList.endDate!), SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)], - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [checkDate(getOffersList.endDate!), SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)]), ], ), ), diff --git a/lib/widgets/app_bar_widget.dart b/lib/widgets/app_bar_widget.dart index 277a414..41a6942 100644 --- a/lib/widgets/app_bar_widget.dart +++ b/lib/widgets/app_bar_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; @@ -27,14 +28,20 @@ AppBar AppBarWidget( GestureDetector( behavior: HitTestBehavior.opaque, onTap: Feedback.wrapForTap(() { - (onBackTapped == null ? Navigator.maybePop(context) : onBackTapped()); + (onBackTapped == null + ? Navigator.maybePop(context) + : onBackTapped()); }, context), - child: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), + child: const Icon( + Icons.arrow_back_ios, + color: MyColors.darkIconColor, + ), ), if (showLogo) 4.width, if (showLogo) SvgPicture.asset(logoPath!), 4.width, - if (!showLogo)title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, + if (!showLogo) + title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, ], ), centerTitle: false, @@ -44,7 +51,18 @@ AppBar AppBarWidget( if (showHomeButton) IconButton( onPressed: () { - onHomeTapped == null ? Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)) : onHomeTapped(); + if (onHomeTapped != null) { + onHomeTapped(); + } else { + Navigator.popUntil( + context, + ModalRoute.withName( + ApiConsts.isDependetLogin + ? AppRoutes.mazayaDiscover + : AppRoutes.dashboard, + ), + ); + } }, icon: const Icon(Icons.home, color: MyColors.darkIconColor), ), @@ -53,7 +71,10 @@ AppBar AppBarWidget( onPressed: () { Navigator.pushNamed(context, AppRoutes.worklistSettings); }, - icon: const Icon(Icons.settings_rounded, color: MyColors.darkIconColor), + icon: const Icon( + Icons.settings_rounded, + color: MyColors.darkIconColor, + ), ), if (showMemberButton) IconButton( diff --git a/lib/widgets/app_in_app_webview_screen.dart b/lib/widgets/app_in_app_webview_screen.dart index 169e087..e0ee934 100644 --- a/lib/widgets/app_in_app_webview_screen.dart +++ b/lib/widgets/app_in_app_webview_screen.dart @@ -50,7 +50,7 @@ class _AppInAppWebViewScreenState extends State { void _onHomeTapped() { if (ApiConsts.isDependetLogin) { - Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboardScreenForDependet)); + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.mazayaDiscover)); } else { Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); }