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/consts.dart'; import 'package:mohem_flutter_app/classes/utils.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'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.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:shimmer/shimmer.dart'; import 'package:url_launcher/url_launcher.dart'; class DashboardScreenForDependet extends StatefulWidget { const DashboardScreenForDependet({Key? key}) : super(key: key); @override State createState() => _DashboardScreenForDependetState(); } class _DashboardScreenForDependetState extends State { List retailersList = []; List _filteredRetailersList = []; List categoriesList = []; String currentCategoryId = "all"; bool isNearestOffers = true; bool isLoading = true; bool isLoadingMore = false; bool isCategoriesLoading = true; bool hasMore = true; bool _isFetchingPage = false; int currentPage = 1; int totalPages = 1; final int pageSize = 10; String searchQuery = ""; final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { await fetchCategories(); } }); } @override void dispose() { _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; // Trigger only when user scrolls to within 35px of the very bottom if (currentScroll >= maxScroll - 35) { if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { fetchNextPage(); } } } Future fetchCategories() async { setState(() { isCategoriesLoading = true; }); try { List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); bool isAr = mounted ? AppState().isArabic(context) : false; WalaPlusCategoryModel allCategory = WalaPlusCategoryModel( id: "all", name: isAr ? "الكل" : "All", ); if (mounted) { setState(() { categoriesList = [allCategory, ...list]; currentCategoryId = "all"; }); } } catch (e) { print("Error fetching WalaPlus categories: $e"); if (mounted && categoriesList.isEmpty) { bool isAr = AppState().isArabic(context); setState(() { categoriesList = [WalaPlusCategoryModel(id: "all", name: isAr ? "الكل" : "All")]; currentCategoryId = "all"; }); } } finally { if (mounted) { setState(() { isCategoriesLoading = false; }); // Default call without category filter ("all" -> passes "") fetchOffers(categoryId: "all"); } } } Future fetchOffers({String? categoryId}) async { String targetCategoryId = (categoryId != null && categoryId.isNotEmpty) ? categoryId : currentCategoryId; setState(() { isLoading = true; currentPage = 1; hasMore = true; }); try { String catParam = (targetCategoryId == "all") ? "" : targetCategoryId; print("Fetching WalaPlus retailers: category='$catParam', page=1, per_page=$pageSize"); WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( categoryId: catParam, page: 1, perPage: pageSize, geoLocation: isNearestOffers ? "24.9485,46.977" : null, context: context, ); retailersList = response?.retailers ?? []; currentPage = 1; totalPages = response?.pagesCount ?? 1; hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false); _runFilter(_searchController.text); } catch (e) { print("Error fetching WalaPlus retailers: $e"); retailersList = []; _runFilter(_searchController.text); } 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 catParam = (currentCategoryId == "all") ? "" : currentCategoryId; print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam'"); WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( categoryId: catParam, page: nextPage, perPage: pageSize, geoLocation: isNearestOffers ? "24.9485,46.977" : null, context: context, ); List newRetailers = response?.retailers ?? []; if (newRetailers.isNotEmpty) { currentPage = nextPage; totalPages = response?.pagesCount ?? totalPages; hasMore = currentPage < totalPages; retailersList.addAll(newRetailers); _runFilter(_searchController.text); } else { hasMore = false; } } catch (e) { print("Error fetching next page of WalaPlus retailers: $e"); } finally { _isFetchingPage = false; if (mounted) { setState(() { isLoadingMore = false; }); } } } Future _onRefresh() async { await fetchCategories(); } void _runFilter(String query) { searchQuery = query; List temp = retailersList; if (query.trim().isNotEmpty) { String q = query.trim().toLowerCase(); temp = temp.where((item) { String name = (item.name ?? "").toLowerCase(); String title = (item.offerTitle ?? "").toLowerCase(); String desc = (item.description ?? "").toLowerCase(); String city = item.displayCity.toLowerCase(); return name.contains(q) || title.contains(q) || desc.contains(q) || city.contains(q); }).toList(); } _filteredRetailersList = temp; setState(() {}); } String _getUserDisplayName() { if (AppState().memberInformationList?.eMPLOYEENAME != null && AppState().memberInformationList!.eMPLOYEENAME!.trim().isNotEmpty) { return AppState().memberInformationList!.eMPLOYEENAME!.trim(); } bool isAr = AppState().isArabic(context); String? dispName = isAr ? AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEAr : AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEEn; if (dispName != null && dispName.trim().isNotEmpty) { return dispName.trim(); } return "Test User"; } void _openRetailer(WalaPlusRetailerModel item) { Navigator.pushNamed( context, AppRoutes.walaPlusRetailerDetails, arguments: item, ); } void _performLogout() { Utils.confirmDialog( context, AppState().isArabic(context) ? "هل أنت متأكد من تسجيل الخروج؟" : "Are you sure you want to log out?", onTap: () { ApiConsts.selectedMemberType = MemberType.employee; ApiConsts.ISDependetLogin = false; AppState().setPostParamsInitConfig(); Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false); }, ); } @override Widget build(BuildContext context) { bool isAr = AppState().isArabic(context); return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, surfaceTintColor: Colors.transparent, automaticallyImplyLeading: false, toolbarHeight: 0, ), body: SafeArea( child: RefreshIndicator( onRefresh: _onRefresh, color: MyColors.textMixColor, child: SingleChildScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top Header: Welcome back & User Name & Logout Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isAr ? "مرحبا بعودتك" : "Welcome back", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), 4.height, Text( _getUserDisplayName(), style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: Color(0xFF111827), letterSpacing: -0.3, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), 10.width, // Logout button InkWell( borderRadius: BorderRadius.circular(10), onTap: _performLogout, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: MyColors.redColor.withOpacity(0.08), borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.logout_rounded, size: 20, color: MyColors.redColor), ), ), ], ), 20.height, // Search Input Box _buildSearchBar(isAr), 18.height, // Nearest Offers Checkbox _buildNearestOffersRow(isAr), 16.height, // Categories Section - Always preserved once loaded with "All" prepended if (isCategoriesLoading && categoriesList.isEmpty) _buildCategoriesShimmer() else if (categoriesList.isNotEmpty) ...[ SizedBox( height: 42.0, width: double.infinity, child: ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: categoriesList.length, separatorBuilder: (cxt, index) => 10.width, itemBuilder: (BuildContext cxt, int index) { WalaPlusCategoryModel item = categoriesList[index]; bool isSelected = currentCategoryId == item.id; String title = item.name ?? ""; return InkWell( borderRadius: BorderRadius.circular(24), onTap: () { if (currentCategoryId != item.id) { setState(() { currentCategoryId = item.id ?? "all"; }); fetchOffers(categoryId: currentCategoryId); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), alignment: Alignment.center, decoration: BoxDecoration( color: isSelected ? const Color(0xFF212C36) : Colors.white, borderRadius: BorderRadius.circular(24), border: Border.all( color: isSelected ? const Color(0xFF212C36) : const Color(0xFFE8ECEF), width: 1.2, ), ), child: Text( title, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF2C3E50), ), ), ), ); }, ), ), 22.height, ], // "Popular offers" Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( isAr ? "أشهر العروض" : "Popular offers", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: Color(0xFF111827), ), ), InkWell( onTap: () { fetchOffers(categoryId: currentCategoryId); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), child: Text( isAr ? "عرض الكل" : "See all", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFE53935), ), ), ), ), ], ), 16.height, // Content Area - Shimmer while loading retailer data if (isLoading) _buildRetailersShimmer() else if (_filteredRetailersList.isNotEmpty) ...[ GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.68, crossAxisSpacing: 14, mainAxisSpacing: 14, ), itemCount: _filteredRetailersList.length, itemBuilder: (context, index) { return _buildOfferCard(_filteredRetailersList[index], isAr); }, ), if (isLoadingMore) ...[ 16.height, const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 12), child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, color: MyColors.textMixColor, ), ), ), ), ], ] else const SizedBox.shrink(), ], ), ), ), ), ); } Widget _buildSearchBar(bool isAr) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( isAr ? "بحث" : "Search", style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF6B7280), ), ), TextField( controller: _searchController, onChanged: _runFilter, decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 3), hintText: isAr ? "بحث" : "Search", hintStyle: const TextStyle( color: Color(0xFF9CA3AF), fontSize: 14, ), border: InputBorder.none, ), style: const TextStyle( color: Color(0xFF111827), fontSize: 14, ), ), ], ), ), const Icon(Icons.search, color: Color(0xFF6B7280), size: 22), ], ), ); } Widget _buildNearestOffersRow(bool isAr) { return InkWell( borderRadius: BorderRadius.circular(8), onTap: () { setState(() { isNearestOffers = !isNearestOffers; }); fetchOffers(categoryId: currentCategoryId); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 22, height: 22, decoration: BoxDecoration( color: isNearestOffers ? const Color(0xFF00A651) : Colors.white, borderRadius: BorderRadius.circular(6), border: Border.all( color: isNearestOffers ? const Color(0xFF00A651) : const Color(0xFFD1D5DB), width: 1.5, ), ), child: isNearestOffers ? const Icon(Icons.check, size: 16, color: Colors.white) : null, ), 10.width, Text( isAr ? "العروض الأقرب" : "Nearest offers", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1F2937), ), ), ], ), ), ); } Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) { String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty) ? item.imageThumb : item.image; String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) ? item.logoThumb : item.logo; return InkWell( borderRadius: BorderRadius.circular(16), onTap: () => _openRetailer(item), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 3), ), ], ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Top Image with Logo Badge overlay SizedBox( height: 110, width: double.infinity, child: Stack( clipBehavior: Clip.none, children: [ // Banner Image Positioned.fill( child: (bannerUrl != null && bannerUrl.isNotEmpty) ? Image.network( bannerUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( color: const Color(0xFFF3F4F6), child: const Icon(Icons.broken_image, color: Color(0xFF9CA3AF), size: 36), ), ) : Container( color: const Color(0xFFF3F4F6), child: const Icon(Icons.image_outlined, color: Color(0xFF9CA3AF), size: 36), ), ), // Brand Logo Badge centered at top if (logoUrl != null && logoUrl.isNotEmpty) Align( alignment: Alignment.topCenter, child: Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFE5E7EB), width: 1), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Image.network( logoUrl, height: 20, width: 40, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const SizedBox(width: 40, height: 20), ), ), ), ], ), ), // Card Body Padding( padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Retailer Name / Bundle Title Text( item.name ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), height: 1.2, ), ), const SizedBox(height: 3), // Offer Title / Subtitle Text( item.offerTitle ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w400, color: Color(0xFF64748B), ), ), const SizedBox(height: 6), // Badges Row 1: "Offer Valid" & "Usage : X" Wrap( spacing: 5, runSpacing: 4, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), decoration: BoxDecoration( color: const Color(0xFFE6F7F0), borderRadius: BorderRadius.circular(10), ), child: Text( isAr ? "العرض ساري" : "Offer Valid", style: const TextStyle( color: Color(0xFF00A651), fontSize: 9.5, fontWeight: FontWeight.w600, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), decoration: BoxDecoration( color: const Color(0xFFFEF3C7), borderRadius: BorderRadius.circular(10), ), child: Text( item.displayUsage, style: const TextStyle( color: Color(0xFFB45309), fontSize: 9.5, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 5), // Location Badge Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), decoration: BoxDecoration( color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(10), ), child: Text( item.displayCity, style: const TextStyle( color: Color(0xFF4B5563), fontSize: 9.5, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ), ), ); } Widget _buildRetailersShimmer() { return Shimmer.fromColors( baseColor: const Color(0xFFE2E8F0), highlightColor: const Color(0xFFF8FAFC), child: GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.68, crossAxisSpacing: 14, mainAxisSpacing: 14, ), itemCount: 4, itemBuilder: (_, __) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Top image placeholder Container( height: 110, width: double.infinity, color: Colors.white, ), Padding( padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( height: 13, width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 3), Container( height: 11, width: 75, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 6), Row( children: [ Container( height: 16, width: 52, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), ), ), const SizedBox(width: 5), Container( height: 16, width: 48, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), ), ), ], ), const SizedBox(height: 5), Container( height: 16, width: 48, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), ), ), ], ), ), ], ), ); }, ), ); } Widget _buildCategoriesShimmer() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 42.0, child: Shimmer.fromColors( baseColor: const Color(0xFFE2E8F0), highlightColor: const Color(0xFFF8FAFC), child: ListView.separated( shrinkWrap: true, scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), itemCount: 5, separatorBuilder: (_, __) => 10.width, itemBuilder: (_, index) { return Container( width: index == 0 ? 80 : (index % 2 == 0 ? 120 : 95), height: 42, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), ), ); }, ), ), ), 22.height, ], ); } }