add search fav

This commit is contained in:
Phuoc Nguyen
2025-11-19 10:50:21 +07:00
parent fc4711a18e
commit 03a7b7940a
4 changed files with 530 additions and 136 deletions

View File

@@ -448,20 +448,20 @@ class AddressesPage extends HookConsumerWidget {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
const SnackBar(
content: Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
const Text('Đã xóa địa chỉ'),
SizedBox(width: 12),
Text('Đã xóa địa chỉ'),
],
),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
backgroundColor: Color(0xFF10B981),
duration: Duration(seconds: 2),
),
);
}

View File

@@ -5,9 +5,10 @@
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
@@ -19,12 +20,13 @@ import 'package:worker/features/products/domain/entities/product.dart';
///
/// Shows all products that the user has marked as favorites.
/// Features:
/// - Search bar for filtering favorites
/// - Grid layout of favorite products
/// - Pull-to-refresh
/// - Empty state when no favorites
/// - Error state with retry
/// - Clear all functionality
class FavoritesPage extends ConsumerWidget {
class FavoritesPage extends HookConsumerWidget {
const FavoritesPage({super.key});
/// Show confirmation dialog before clearing all favorites
@@ -76,18 +78,40 @@ class FavoritesPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Search controller
final searchController = useTextEditingController();
final searchQuery = useState('');
// Watch favorites and products
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
final favoriteCount = ref.watch(favoriteCountProvider);
// Track if we've loaded data at least once to prevent empty state flash
final hasLoadedOnce = favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
final hasLoadedOnce =
favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
// Filter products based on search query
List<Product> filterProducts(List<Product> products) {
if (searchQuery.value.isEmpty) return products;
final query = searchQuery.value.toLowerCase().trim();
return products.where((product) {
final matchesName = product.name.toLowerCase().contains(query);
final matchesSku =
product.erpnextItemCode?.toLowerCase().contains(query) ?? false;
return matchesName || matchesSku;
}).toList();
}
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text('Yêu thích', style: TextStyle(color: Colors.black)),
@@ -115,7 +139,11 @@ class FavoritesPage extends ConsumerWidget {
// Clear all button
if (favoriteCount > 0)
IconButton(
icon: const FaIcon(FontAwesomeIcons.trashCan, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.trashCan,
color: Colors.black,
size: 20,
),
tooltip: 'Xóa tất cả',
onPressed: () => _showClearAllDialog(context, ref, favoriteCount),
),
@@ -125,6 +153,9 @@ class FavoritesPage extends ConsumerWidget {
body: SafeArea(
child: favoriteProductsAsync.when(
data: (products) {
// Filter products based on search
final filteredProducts = filterProducts(products);
// IMPORTANT: Only show empty state after we've confirmed data loaded
// This prevents empty state flash during initial load
if (products.isEmpty && hasLoadedOnce) {
@@ -136,12 +167,119 @@ class FavoritesPage extends ConsumerWidget {
return const _LoadingState();
}
return RefreshIndicator(
onRefresh: () async {
// Use the new refresh method from AsyncNotifier
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: _FavoritesGrid(products: products),
return Column(
children: [
// Search Bar
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: TextField(
controller: searchController,
onChanged: (value) => searchQuery.value = value,
decoration: InputDecoration(
hintText: 'Tìm kiếm sản phẩm...',
hintStyle: const TextStyle(color: AppColors.grey500),
prefixIcon: const Icon(
Icons.search,
color: AppColors.grey500,
),
suffixIcon: searchQuery.value.isNotEmpty
? IconButton(
icon: const Icon(
Icons.clear,
color: AppColors.grey500,
),
onPressed: () {
searchController.clear();
searchQuery.value = '';
},
)
: null,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
),
),
),
// Results count when searching
if (searchQuery.value.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Tìm thấy ${filteredProducts.length} sản phẩm',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
),
),
// Products Grid
Expanded(
child:
filteredProducts.isEmpty && searchQuery.value.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FaIcon(
FontAwesomeIcons.magnifyingGlass,
size: 64,
color: AppColors.grey500,
),
const SizedBox(height: AppSpacing.md),
Text(
'Không tìm thấy "${searchQuery.value}"',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: AppSpacing.sm),
const Text(
'Thử tìm kiếm với từ khóa khác',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
)
: RefreshIndicator(
onRefresh: () async {
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: filteredProducts),
),
),
],
);
},
loading: () {
@@ -156,7 +294,9 @@ class FavoritesPage extends ConsumerWidget {
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: previousValue),
),
@@ -177,7 +317,9 @@ class FavoritesPage extends ConsumerWidget {
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text('Đang tải...'),
@@ -212,7 +354,9 @@ class FavoritesPage extends ConsumerWidget {
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: previousValue),
),
@@ -241,7 +385,9 @@ class FavoritesPage extends ConsumerWidget {
),
TextButton(
onPressed: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: const Text(
'Thử lại',