Added sub categories

master
FaizHashmi 2 weeks ago
parent 821cdf474d
commit 154b62bbad

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.8331 11.6668C5.83305 11.7752 5.86321 11.8815 5.9202 11.9737C5.97718 12.066 6.05874 12.1405 6.15572 12.1889L7.3225 12.7723C7.41146 12.8167 7.51031 12.8377 7.60967 12.8332C7.70902 12.8287 7.80557 12.7989 7.89015 12.7466C7.97473 12.6943 8.04454 12.6212 8.09293 12.5343C8.14133 12.4474 8.16671 12.3496 8.16666 12.2502V8.16678C8.16679 7.87766 8.27427 7.5989 8.46827 7.38452L12.6821 2.72418C12.7576 2.64051 12.8073 2.53675 12.8251 2.42544C12.8429 2.31414 12.828 2.20007 12.7824 2.09702C12.7367 1.99396 12.6622 1.90635 12.5678 1.84477C12.4733 1.78318 12.3631 1.75027 12.2504 1.75H1.74938C1.63655 1.75004 1.52616 1.78279 1.43158 1.84429C1.33699 1.90579 1.26226 1.99339 1.21645 2.09648C1.17064 2.19958 1.15571 2.31375 1.17347 2.42516C1.19123 2.53656 1.24091 2.64043 1.3165 2.72418L5.53149 7.38452C5.72549 7.5989 5.83297 7.87766 5.8331 8.16678V11.6668Z" stroke-linecap="round" stroke="#202B33"/>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

@ -810,6 +810,22 @@
"shareAccessGuide": "مشاركة دليل الوصول",
"nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة",
"multipleCities": "مدن متعددة",
"filters": "الفلاتر",
"availability": "التوفر",
"chooseOneOrBoth": "اختر واحدًا أو كليهما",
"sortBy": "ترتيب حسب",
"recommended": "موصى به",
"nearest": "الأقرب",
"highestDiscount": "أعلى خصم",
"newest": "الأحدث",
"distance": "المسافة",
"fromYourCurrentLocation": "من موقعك الحالي",
"any": "الكل",
"minimumDiscount": "الحد الأدنى للخصم",
"reset": "إعادة تعيين",
"applyFilters": "تطبيق الفلاتر",
"distanceKm": "{} كم",
"discountPercentPlus": "{}%+",
"visitMerchantWebsite": "زيارة موقع التاجر",
"merchantWebsiteNotAvailable": "موقع التاجر غير متوفر"
}

@ -810,6 +810,22 @@
"shareAccessGuide": "Share Access Guide",
"nationalIdOrIqamaNumber": "National ID or Iqama Number",
"multipleCities": "Multiple Cities",
"filters": "Filters",
"availability": "Availability",
"chooseOneOrBoth": "Choose one or both",
"sortBy": "Sort by",
"recommended": "Recommended",
"nearest": "Nearest",
"highestDiscount": "Highest discount",
"newest": "Newest",
"distance": "Distance",
"fromYourCurrentLocation": "From your current location",
"any": "Any",
"minimumDiscount": "Minimum discount",
"reset": "Reset",
"applyFilters": "Apply filters",
"distanceKm": "{} km",
"discountPercentPlus": "{}%+",
"visitMerchantWebsite": "Visit Merchant Website",
"merchantWebsiteNotAvailable": "Merchant website not available"
}

@ -181,7 +181,9 @@ class OffersAndDiscountsApiClient {
map.removeWhere((key, value) => value == null);
}
Future<List<WalaPlusCategoryModel>> getWalaPlusCategories({BuildContext? context, String? mobileNumber}) async {
// Pass parentId to fetch the subcategories under a given main category (WP_ParentId) -
// omit it (the default) to fetch the top-level main categories, same as before.
Future<List<WalaPlusCategoryModel>> getWalaPlusCategories({BuildContext? context, String? mobileNumber, String? parentId}) async {
String url = ApiConsts.walaPlusCategories;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
@ -196,6 +198,9 @@ class OffersAndDiscountsApiClient {
}
postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1;
postParams["CompanyID"] = 1;
if (parentId != null && parentId.trim().isNotEmpty) {
postParams["WP_Parent_Id"] = parentId.trim();
}
_cleanWalaPlusParams(postParams);
return await ApiClient().postJsonForObject(

@ -4,12 +4,12 @@ class ApiConsts {
// static String baseUrl = "http://10.201.214.101:2027"; // Local server
// static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver
static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver
// static String baseUrl = "http://10.201.204.101:2024";
// static String baseUrl = "https://webservices.hmg.com"; // PreProd
// static String baseUrl = "https://hmgwebservices.com"; // Live server
static String baseUrl = "https://mohemm.hmg.com"; // New Live server
// static String baseUrl = "https://mohemm.hmg.com"; // New Live server
static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server
//
// static String baseUrl = "https://uat.hmgwebservices.com"; //

@ -821,7 +821,23 @@ class CodegenLoader extends AssetLoader{
"microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.",
"shareAccessGuide": "مشاركة دليل الوصول",
"nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة",
"multipleCities": "مدن متعددة"
"multipleCities": "مدن متعددة",
"filters": "الفلاتر",
"availability": "التوفر",
"chooseOneOrBoth": "اختر واحدًا أو كليهما",
"sortBy": "ترتيب حسب",
"recommended": "موصى به",
"nearest": "الأقرب",
"highestDiscount": "أعلى خصم",
"newest": "الأحدث",
"distance": "المسافة",
"fromYourCurrentLocation": "من موقعك الحالي",
"any": "الكل",
"minimumDiscount": "الحد الأدنى للخصم",
"reset": "إعادة تعيين",
"applyFilters": "تطبيق الفلاتر",
"distanceKm": "{} كم",
"discountPercentPlus": "{}%+"
};
static const Map<String,dynamic> _en_US = {
"mohemm": "Mohemm",
@ -1630,7 +1646,23 @@ static const Map<String,dynamic> _en_US = {
"microphoneAccessRequired": "The app needs microphone access to be able to record audio.",
"shareAccessGuide": "Share Access Guide",
"nationalIdOrIqamaNumber": "National ID or Iqama Number",
"multipleCities": "Multiple Cities"
"multipleCities": "Multiple Cities",
"filters": "Filters",
"availability": "Availability",
"chooseOneOrBoth": "Choose one or both",
"sortBy": "Sort by",
"recommended": "Recommended",
"nearest": "Nearest",
"highestDiscount": "Highest discount",
"newest": "Newest",
"distance": "Distance",
"fromYourCurrentLocation": "From your current location",
"any": "Any",
"minimumDiscount": "Minimum discount",
"reset": "Reset",
"applyFilters": "Apply filters",
"distanceKm": "{} km",
"discountPercentPlus": "{}%+"
};
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
}

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
abstract class LocaleKeys {
static const mohemm = 'mohemm';
static const english = 'english';
static const arabic = 'arabic';
@ -258,7 +258,7 @@ abstract class LocaleKeys {
static const Payroll = 'Payroll';
static const civilIdentityNumber = 'civilIdentityNumber';
static const dateOfBirth = 'dateOfBirth';
static const maritalStatus = 'maritalStatus ';
static const maritalStatus = 'maritalStatus ';
static const fullName = 'fullName';
static const remove = 'remove';
static const submit = 'submit';
@ -289,7 +289,7 @@ abstract class LocaleKeys {
static const enterNewInfo = 'enterNewInfo';
static const endDate = 'endDate';
static const removeThisMember = 'removeThisMember';
static const wantUpdateThisMember = 'wantUpdateThisMember ';
static const wantUpdateThisMember = 'wantUpdateThisMember ';
static const addNewFamilyMember = 'addNewFamilyMember';
static const addRow = 'addRow';
static const pleaseSelect = 'pleaseSelect';
@ -794,5 +794,20 @@ abstract class LocaleKeys {
static const shareAccessGuide = 'shareAccessGuide';
static const nationalIdOrIqamaNumber = 'nationalIdOrIqamaNumber';
static const multipleCities = 'multipleCities';
static const filters = 'filters';
static const availability = 'availability';
static const chooseOneOrBoth = 'chooseOneOrBoth';
static const sortBy = 'sortBy';
static const recommended = 'recommended';
static const nearest = 'nearest';
static const highestDiscount = 'highestDiscount';
static const newest = 'newest';
static const distance = 'distance';
static const fromYourCurrentLocation = 'fromYourCurrentLocation';
static const any = 'any';
static const minimumDiscount = 'minimumDiscount';
static const reset = 'reset';
static const applyFilters = 'applyFilters';
static const distanceKm = 'distanceKm';
static const discountPercentPlus = 'discountPercentPlus';
}

@ -557,7 +557,6 @@ class WalaPlusRetailerModel {
// Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of
// just picking the first one found, so the badge doesn't silently hide the other branches.
String getDisplayCity({bool isAr = false}) {
log("locations: ${locations?.map((e) => e.toJson()).toList()}");
if (locations == null || locations!.isEmpty) return '';
Set<String> distinctCities = <String>{};

@ -18,6 +18,7 @@ import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category
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/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';
import 'package:shimmer/shimmer.dart';
@ -34,6 +35,22 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
List<WalaPlusCategoryModel> categoriesList = [];
String currentCategoryId = "";
String selectedTypeFilter = "all";
// 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.
List<WalaPlusCategoryModel> subcategoriesList = <WalaPlusCategoryModel>[];
bool isSubcategoriesLoading = false;
String? _selectedSubcategoryId;
// Filters bottom sheet state. Availability is single-select and functional - it drives the
// same selectedTypeFilter/offerType param the old chip row used. Sort/Distance/Minimum
// discount still have no backend support, so those remain UI-only for now. Committed on
// "Apply filters"; sort/distance/discount are persisted here so reopening the sheet shows
// the last-applied selection (availability's source of truth is selectedTypeFilter itself).
String _filterSortBy = 'nearest';
String _filterDistance = '5km';
String _filterMinDiscount = 'any';
bool isNearestOffers = true;
bool isLoading = true;
bool isLoadingMore = false;
@ -49,23 +66,6 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
final ScrollController _scrollController = ScrollController();
bool _hasRegisteredMobileLoginInfo = false;
List<String> get _availableTypeFilters {
Set<String> types = <String>{};
for (WalaPlusRetailerModel r in retailersList) {
String? t = r.normalizedType;
if (t != null && t.isNotEmpty) {
types.add(t);
}
}
List<String> list = <String>['all', 'instore', 'online'];
for (String t in types) {
if (!list.contains(t)) {
list.add(t);
}
}
return list;
}
String _getTypeFilterTitle(String type) {
if (type == 'all') {
return LocaleKeys.allCategories.tr();
@ -239,6 +239,45 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
}
}
// 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<void> fetchSubcategories(String? parentId) async {
if (parentId == null || parentId.isEmpty || parentId == "all") {
setState(() {
subcategoriesList = <WalaPlusCategoryModel>[];
_selectedSubcategoryId = null;
});
return;
}
setState(() {
isSubcategoriesLoading = true;
});
try {
List<WalaPlusCategoryModel> list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context, parentId: parentId);
if (mounted) {
setState(() {
subcategoriesList = list;
_selectedSubcategoryId = null;
});
}
} catch (e) {
debugPrint("Error fetching WalaPlus subcategories: $e");
if (mounted) {
setState(() {
subcategoriesList = <WalaPlusCategoryModel>[];
});
}
} finally {
if (mounted) {
setState(() {
isSubcategoriesLoading = false;
});
}
}
}
Future<void> fetchOffers({String? categoryId, String? query}) async {
String targetCategoryId = categoryId ?? (currentCategoryId.isEmpty ? "all" : currentCategoryId);
if (query != null) {
@ -592,7 +631,7 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
if (categoriesList.isNotEmpty) _buildCategoriesList(isAr) else _buildCategoriesShimmer(),
// Type Filter Chips (All, In Store, Online)
_buildTypeFilterChips(isAr),
_buildFiltersRow(isAr),
const SizedBox(height: 16),
@ -711,6 +750,7 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
currentCategoryId = item.id ?? "";
});
fetchOffers(categoryId: currentCategoryId);
fetchSubcategories(currentCategoryId);
}
},
child: SizedBox(
@ -736,54 +776,280 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
);
}
Widget _buildTypeFilterChips(bool isAr) {
List<String> types = _availableTypeFilters;
// Replaces the old All/In Store/Online chip row: a fixed "Filters" chip (opens the filters
// bottom sheet) + a divider, then a horizontally scrollable row of subcategory chips fetched
// from the API for whichever main category is currently selected (see fetchSubcategories).
Widget _buildFiltersRow(bool isAr) {
// "All Categories" (or nothing selected) has no subcategories to show at all - only a
// specific main category, once it has actually loaded some, gets the divider + chip row.
bool showSubcategoriesRow = currentCategoryId.isNotEmpty && currentCategoryId != "all" && (isSubcategoriesLoading || subcategoriesList.isNotEmpty);
return Padding(
padding: const EdgeInsets.only(top: 14, bottom: 2),
child: SizedBox(
height: 38,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
itemCount: types.length,
separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8),
itemBuilder: (BuildContext context, int index) {
String typeKey = types[index];
bool isSelected = selectedTypeFilter == typeKey;
String title = _getTypeFilterTitle(typeKey);
IconData icon = _getTypeFilterIcon(typeKey);
return InkWell(
child: Row(
children: <Widget>[
InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () {
if (selectedTypeFilter != typeKey) {
setState(() {
selectedTypeFilter = typeKey;
});
fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
}
},
onTap: _showFiltersBottomSheet,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
alignment: Alignment.center,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: MyColors.lightGreyEAColor),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(icon, size: 14, color: isSelected ? MyColors.white : MyColors.black),
SvgPicture.asset("assets/icons/filter_icon.svg", width: 14, height: 14, colorFilter: const ColorFilter.mode(MyColors.black, BlendMode.srcIn)),
const SizedBox(width: 5),
title.toText12(color: isSelected ? MyColors.white : MyColors.black),
LocaleKeys.filters.tr().toText12(color: MyColors.black),
],
),
),
);
},
),
if (showSubcategoriesRow) ...<Widget>[
Container(margin: const EdgeInsets.symmetric(horizontal: 10), width: 1, height: 20, color: const Color(0xFFE2E8F0)),
Expanded(
child:
isSubcategoriesLoading
? const Center(child: SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF94A3B8))))
: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
// +1 for the leading "All" chip (clears the subcategory selection,
// falling back to just the parent main category).
itemCount: subcategoriesList.length + 1,
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 ?? "");
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () {
setState(() {
_selectedSubcategoryId = isAllChip ? null : (isSelected ? null : subcategory!.id);
});
// Selecting a subcategory filters offers to just that subcategory;
// "All" (or deselecting) falls back to the parent main category.
fetchOffers(categoryId: _selectedSubcategoryId ?? currentCategoryId);
},
child: Container(
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),
),
);
},
),
),
],
],
),
),
);
}
// Matches the shared Filters bottom sheet design. Availability is single-select and
// functional, driving selectedTypeFilter/offerType exactly like the old chip row did.
// Sort/Distance/Minimum discount still have no backend support, so those remain UI-only.
void _showFiltersBottomSheet() {
String draftAvailability = selectedTypeFilter;
String draftSortBy = _filterSortBy;
String draftDistance = _filterDistance;
String draftMinDiscount = _filterMinDiscount;
const Color darkBg = Color(0xFF202B33);
const Color borderColor = Color(0xFFE2E8F0);
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext sheetContext) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setSheetState) {
Widget buildOption(String label, bool isSelected, VoidCallback onTap, {IconData? icon}) {
return Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isSelected ? darkBg : Colors.white,
border: Border.all(color: isSelected ? darkBg : borderColor),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (icon != null) ...<Widget>[Icon(icon, size: 14, color: isSelected ? Colors.white : const Color(0xFF64748B)), const SizedBox(width: 5)],
Flexible(child: label.toText13(isBold: true, color: isSelected ? Colors.white : darkBg, maxlines: 1)),
if (isSelected) ...<Widget>[const SizedBox(width: 5), const Icon(Icons.check, size: 14, color: Colors.white)],
],
),
),
),
);
}
Widget sectionHeader(String title, {String? hint}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
title.toText14(isBold: true, color: darkBg),
if (hint != null) hint.toText12(color: const Color(0xFF94A3B8)),
],
);
}
return SafeArea(
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))),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Center(
child: Container(width: 44, height: 4.5, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5))),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.filters.tr().toText20(isBold: true, color: darkBg),
InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => Navigator.pop(sheetContext),
child: Container(
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),
),
),
],
),
20.height,
// Availability - single select, drives the real offerType filter.
LocaleKeys.availability.tr().toText14(isBold: true, color: darkBg),
10.height,
Row(
children: <Widget>[
buildOption(LocaleKeys.allCategories.tr(), draftAvailability == 'all', () => setSheetState(() => draftAvailability = 'all')),
const SizedBox(width: 10),
buildOption(LocaleKeys.online.tr(), draftAvailability == 'online', () => setSheetState(() => draftAvailability = 'online')),
const SizedBox(width: 10),
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),
10.height,
Row(
children: <Widget>[
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')),
],
),
20.height,
// Distance - single select.
sectionHeader(LocaleKeys.distance.tr(), hint: LocaleKeys.fromYourCurrentLocation.tr()),
10.height,
Row(
children: <Widget>[
buildOption(LocaleKeys.any.tr(), draftDistance == 'any', () => setSheetState(() => draftDistance = 'any')),
const SizedBox(width: 8),
buildOption(LocaleKeys.distanceKm.tr(args: <String>['5']), draftDistance == '5km', () => setSheetState(() => draftDistance = '5km')),
const SizedBox(width: 8),
buildOption(LocaleKeys.distanceKm.tr(args: <String>['10']), draftDistance == '10km', () => setSheetState(() => draftDistance = '10km')),
const SizedBox(width: 8),
buildOption(LocaleKeys.distanceKm.tr(args: <String>['25']), draftDistance == '25km', () => setSheetState(() => draftDistance = '25km')),
],
),
20.height,
// Minimum discount - single select.
LocaleKeys.minimumDiscount.tr().toText14(isBold: true, color: darkBg),
10.height,
Row(
children: <Widget>[
buildOption(LocaleKeys.any.tr(), draftMinDiscount == 'any', () => setSheetState(() => draftMinDiscount = 'any')),
const SizedBox(width: 8),
buildOption(LocaleKeys.discountPercentPlus.tr(args: <String>['10']), draftMinDiscount == '10', () => setSheetState(() => draftMinDiscount = '10')),
const SizedBox(width: 8),
buildOption(LocaleKeys.discountPercentPlus.tr(args: <String>['20']), draftMinDiscount == '20', () => setSheetState(() => draftMinDiscount = '20')),
const SizedBox(width: 8),
buildOption(LocaleKeys.discountPercentPlus.tr(args: <String>['30']), draftMinDiscount == '30', () => setSheetState(() => draftMinDiscount = '30')),
],
),
20.height,
const Divider(height: 1, thickness: 1, color: borderColor),
16.height,
Row(
children: <Widget>[
Expanded(
child: DefaultButton(
LocaleKeys.reset.tr(),
() {
setSheetState(() {
draftAvailability = 'all';
draftSortBy = 'newest';
draftDistance = 'any';
draftMinDiscount = 'any';
});
setState(() {
selectedTypeFilter = draftAvailability;
_filterSortBy = draftSortBy;
_filterDistance = draftDistance;
_filterMinDiscount = draftMinDiscount;
});
Navigator.pop(sheetContext);
fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
},
colors: const <Color>[Colors.white, Colors.white],
border: Border.all(color: borderColor),
textColor: darkBg,
),
),
const SizedBox(width: 12),
Expanded(
child: DefaultButton(LocaleKeys.applyFilters.tr(), () {
setState(() {
selectedTypeFilter = draftAvailability;
_filterSortBy = draftSortBy;
_filterDistance = draftDistance;
_filterMinDiscount = draftMinDiscount;
});
Navigator.pop(sheetContext);
fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
}),
),
],
),
],
),
),
);
},
);
},
);
}
Widget _buildCategoryCard(WalaPlusCategoryModel item, bool isSelected) {
const double cardSize = 84.0;
const double borderRadiusVal = 18.0;

@ -22,6 +22,7 @@ import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dar
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/info_chip_widget.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shimmer/shimmer.dart';
import 'package:url_launcher/url_launcher.dart';
class WalaPlusRetailerDetailsScreen extends StatefulWidget {
@ -173,10 +174,7 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
await Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => 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) {
@ -521,7 +519,7 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
),
body:
(_isLoading && !_hasDetailsData)
? Center(child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 96.0, width: 96.0))
? _buildDetailsShimmer()
: Column(
children: [
if (_isLoading && _hasDetailsData) const LinearProgressIndicator(minHeight: 2.5, backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF168A89))),
@ -575,17 +573,12 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: (_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B)),
),
Expanded(child: (_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B))),
const SizedBox(width: 12),
Container(
width: 44,
height: 44,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFFF1F5F9),
),
decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFFF1F5F9)),
child: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@ -615,11 +608,7 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
14.height,
InkWell(
onTap: _openMerchantWebsite,
child: LocaleKeys.visitMerchantWebsite.tr().toText14(
color: const Color(0xFF0D9488),
isUnderLine: true,
weight: FontWeight.w600,
),
child: LocaleKeys.visitMerchantWebsite.tr().toText14(color: const Color(0xFF0D9488), isUnderLine: true, weight: FontWeight.w600),
),
],
10.height,
@ -671,6 +660,80 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
);
}
// Skeleton mirroring the real card layout (banner, brand strip, name, badges) plus two
// generic placeholder cards for the locations/contact sections below it.
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 Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Banner
block(width: double.infinity, height: 190, radius: 16),
14.height,
// Brand strip
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(children: <Widget>[block(width: 36, height: 36, radius: 18), const SizedBox(width: 10), block(width: 90, height: 14)]),
block(width: 48, height: 48, radius: 10),
],
),
18.height,
// Retailer name
block(width: 180, height: 20),
18.height,
// Info badges row
Row(children: <Widget>[block(width: 90, height: 28, radius: 8), const SizedBox(width: 8), block(width: 70, height: 28, radius: 8)]),
],
),
),
16.height,
// Locations section placeholder
Container(
width: double.infinity,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[block(width: 120, height: 16), 14.height, block(width: double.infinity, height: 56, radius: 12), 10.height, block(width: double.infinity, height: 56, radius: 12)],
),
),
16.height,
// Contact section placeholder
Container(
width: double.infinity,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
block(width: 120, height: 16),
14.height,
Row(children: <Widget>[block(width: 32, height: 32, radius: 16), const SizedBox(width: 10), block(width: 160, height: 14)]),
],
),
),
],
),
),
);
}
Widget _buildBrandStrip(bool isAr) {
String? logoUrl = _retailer?.displayLogo;
return Row(
@ -711,11 +774,10 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
// One Type Chip per distinct type - Online (green) and/or In Store (blue).
for (String offerType in offerTypes)
InfoChipWidget(
text: offerType == 'online'
? LocaleKeys.online.tr()
: (offerType == 'instore'
? LocaleKeys.inStore.tr()
: (offerType.isNotEmpty ? (offerType[0].toUpperCase() + offerType.substring(1)) : offerType)),
text:
offerType == 'online'
? LocaleKeys.online.tr()
: (offerType == 'instore' ? LocaleKeys.inStore.tr() : (offerType.isNotEmpty ? (offerType[0].toUpperCase() + offerType.substring(1)) : offerType)),
icon: offerType == 'online' ? Icons.language_rounded : Icons.storefront_outlined,
backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF),
textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB),

Loading…
Cancel
Save