You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mohemm-flutter-app/lib/ui/landing/dashboard_screen_for_depend...

881 lines
32 KiB
Dart

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<DashboardScreenForDependet> createState() => _DashboardScreenForDependetState();
}
class _DashboardScreenForDependetState extends State<DashboardScreenForDependet> {
List<WalaPlusRetailerModel> retailersList = [];
List<WalaPlusRetailerModel> _filteredRetailersList = [];
List<WalaPlusCategoryModel> 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<void> fetchCategories() async {
setState(() {
isCategoriesLoading = true;
});
try {
List<WalaPlusCategoryModel> 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<void> 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<void> 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<WalaPlusRetailerModel> 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<void> _onRefresh() async {
await fetchCategories();
}
void _runFilter(String query) {
searchQuery = query;
List<WalaPlusRetailerModel> 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<dynamic> 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,
],
);
}
}