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/mazaya/saved_retailers_screen.dart

387 lines
13 KiB
Dart

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<SavedRetailersScreen> createState() => _SavedRetailersScreenState();
}
class _SavedRetailersScreenState extends State<SavedRetailersScreen> {
List<WalaPlusRetailerModel> favoriteRetailers = <WalaPlusRetailerModel>[];
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<void> 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 ?? <WalaPlusRetailerModel>[];
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 = <WalaPlusRetailerModel>[];
} 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? 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<WalaPlusRetailerModel> newRetailers =
response?.retailers ?? <WalaPlusRetailerModel>[];
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<void> _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: <Widget>[
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>[
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: <Widget>[
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<TextEditingValue>(
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: <Widget>[
WalaPlusRetailerGrid(
isLoading: isLoading,
retailers: favoriteRetailers,
isAr: isAr,
isLoadingMore: isLoadingMore,
onTapRetailer: _openRetailer,
onFavoriteTap: _toggleFavorite,
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
);
}
}