update perf

This commit is contained in:
Phuoc Nguyen
2025-12-02 17:32:20 +07:00
parent 211ebdf1d8
commit fc9b5e967f
13 changed files with 254 additions and 200 deletions

View File

@@ -10,6 +10,7 @@ library;
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -569,10 +570,10 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
@riverpod @riverpod
LoggingInterceptor loggingInterceptor(Ref ref) { LoggingInterceptor loggingInterceptor(Ref ref) {
// Only enable logging in debug mode // Only enable logging in debug mode
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter const bool isDebug = kDebugMode; // TODO: Replace with kDebugMode from Flutter
return LoggingInterceptor( return LoggingInterceptor(
enableRequestLogging: false, enableRequestLogging: true,
enableResponseLogging: isDebug, enableResponseLogging: isDebug,
enableErrorLogging: isDebug, enableErrorLogging: isDebug,
); );

View File

@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
} }
String _$loggingInterceptorHash() => String _$loggingInterceptorHash() =>
r'6afa480caa6fcd723dab769bb01601b8a37e20fd'; r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
/// Provider for ErrorTransformerInterceptor /// Provider for ErrorTransformerInterceptor

View File

@@ -13,6 +13,7 @@ import 'dart:developer' as developer;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -248,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor {
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final curl = _cURLRepresentation(options); final curl = _cURLRepresentation(options);
// debugPrint( debugPrint(
// '╔╣ CURL Request ╠══════════════════════════════════════════════════', '╔╣ CURL Request ╠══════════════════════════════════════════════════',
// ); );
// debugPrint(curl); debugPrint(curl);
// debugPrint( debugPrint(
// '╚═════════════════════════════════════════════════════════════════', '╚═════════════════════════════════════════════════════════════════',
// ); );
// Also log to dart:developer for better filtering in DevTools // Also log to dart:developer for better filtering in DevTools
developer.log(curl, name: 'DIO_CURL', time: DateTime.now()); developer.log(curl, name: 'DIO_CURL', time: DateTime.now());
handler.next(options); handler.next(options);
@@ -467,7 +468,7 @@ Future<Dio> dio(Ref ref) async {
// Add interceptors in order // Add interceptors in order
// 1. Custom Curl interceptor (first to log cURL commands) // 1. Custom Curl interceptor (first to log cURL commands)
// Uses debugPrint and developer.log for better visibility // Uses debugPrint and developer.log for better visibility
..interceptors.add(CustomCurlLoggerInterceptor()) // ..interceptors.add(CustomCurlLoggerInterceptor())
// 2. Logging interceptor // 2. Logging interceptor
..interceptors.add(ref.watch(loggingInterceptorProvider)) ..interceptors.add(ref.watch(loggingInterceptorProvider))
// 3. Auth interceptor (add tokens to requests) // 3. Auth interceptor (add tokens to requests)

View File

@@ -131,7 +131,7 @@ final class DioProvider
} }
} }
String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4'; String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
/// Provider for DioClient /// Provider for DioClient

View File

@@ -87,7 +87,7 @@ class FrappeAuthService {
} }
} }
final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}'; const url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
// Build cookie header // Build cookie header
final storedSession = await getStoredSession(); final storedSession = await getStoredSession();

View File

@@ -6,7 +6,6 @@
/// Uses Riverpod 3.0 with code generation for type-safe state management. /// Uses Riverpod 3.0 with code generation for type-safe state management.
library; library;
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/core/constants/api_constants.dart';
@@ -14,7 +13,6 @@ import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_service.dart'; import 'package:worker/core/services/frappe_auth_service.dart';
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/user.dart'; import 'package:worker/features/auth/domain/entities/user.dart';
part 'auth_provider.g.dart'; part 'auth_provider.g.dart';
@@ -80,10 +78,6 @@ class Auth extends _$Auth {
Future<FrappeAuthService> get _frappeAuthService async => Future<FrappeAuthService> get _frappeAuthService async =>
await ref.read(frappeAuthServiceProvider.future); await ref.read(frappeAuthServiceProvider.future);
/// Get auth remote data source
Future<AuthRemoteDataSource> get _remoteDataSource async =>
await ref.read(authRemoteDataSourceProvider.future);
/// Initialize with saved session if available /// Initialize with saved session if available
@override @override
Future<User?> build() async { Future<User?> build() async {
@@ -170,7 +164,6 @@ class Auth extends _$Auth {
} }
final frappeService = await _frappeAuthService; final frappeService = await _frappeAuthService;
final remoteDataSource = await _remoteDataSource;
// Get current session (should exist from app startup) // Get current session (should exist from app startup)
final currentSession = await frappeService.getStoredSession(); final currentSession = await frappeService.getStoredSession();
@@ -183,22 +176,8 @@ class Auth extends _$Auth {
} }
} }
// Get stored session again // Call login API and store session
final session = await frappeService.getStoredSession(); final loginResponse = await frappeService.login(phoneNumber, password: password);
if (session == null) {
throw Exception('Session not available');
}
// Call login API with current session
final loginResponse = await remoteDataSource.login(
phone: phoneNumber,
csrfToken: session['csrfToken']!,
sid: session['sid']!,
password: password, // Reserved for future use
);
// Update FlutterSecureStorage with new authenticated session
await frappeService.login(phoneNumber, password: password);
// Save rememberMe preference // Save rememberMe preference
await _localDataSource.saveRememberMe(rememberMe); await _localDataSource.saveRememberMe(rememberMe);

View File

@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth(); Auth create() => Auth();
} }
String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae'; String _$authHash() => r'd851980cad7a624f00eba69e19d8a4fee22008e7';
/// Authentication Provider /// Authentication Provider
/// ///

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/utils/extensions.dart'; import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
@@ -133,10 +134,7 @@ class _HomePageState extends ConsumerState<HomePage> {
}, },
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
loading: () => const Padding( loading: () => _buildPromotionsShimmer(colorScheme),
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => const SizedBox.shrink(), error: (error, stack) => const SizedBox.shrink(),
), ),
), ),
@@ -241,4 +239,93 @@ class _HomePageState extends ConsumerState<HomePage> {
), ),
); );
} }
/// Build shimmer loading for promotions section
Widget _buildPromotionsShimmer(ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title shimmer
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Chương trình ưu đãi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
const SizedBox(height: 12),
// Cards shimmer
SizedBox(
height: 210,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 3,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: colorScheme.surfaceContainerHighest,
highlightColor: colorScheme.surface,
child: Container(
width: 280,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image placeholder
Container(
height: 140,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
),
),
// Text placeholders
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 200,
height: 16,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
width: 140,
height: 12,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
),
);
},
),
),
],
),
);
}
} }

View File

@@ -1,23 +1,25 @@
/// Provider: Promotions Provider /// Provider: Promotions Provider
/// ///
/// Manages the state of promotions data using Riverpod. /// Manages the state of promotions data using Riverpod.
/// Provides access to active promotions throughout the app. /// Uses the same data source as news articles (single API call).
/// ///
/// Uses AsyncNotifierProvider for automatic loading, error, and data states. /// Uses AsyncNotifierProvider for automatic loading, error, and data states.
library; library;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/home/data/datasources/home_local_datasource.dart';
import 'package:worker/features/home/data/repositories/home_repository_impl.dart';
import 'package:worker/features/home/domain/entities/promotion.dart'; import 'package:worker/features/home/domain/entities/promotion.dart';
import 'package:worker/features/home/domain/usecases/get_promotions.dart'; import 'package:worker/features/news/presentation/providers/news_provider.dart';
part 'promotions_provider.g.dart'; part 'promotions_provider.g.dart';
/// Max number of promotions to display on home page
const int _maxPromotions = 5;
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -31,33 +33,22 @@ part 'promotions_provider.g.dart';
/// ); /// );
/// ``` /// ```
@riverpod @riverpod
class PromotionsNotifier extends _$PromotionsNotifier { Future<List<Promotion>> promotions(Ref ref) async {
@override // Use newsArticles provider (same API call, no duplicate request)
Future<List<Promotion>> build() async { final articles = await ref.watch(newsArticlesProvider.future);
// Initialize dependencies
final localDataSource = const HomeLocalDataSourceImpl();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
// Fetch promotions (only active ones) // Take max 5 articles and convert to Promotion
return await useCase(); final limitedArticles = articles.take(_maxPromotions).toList();
}
return limitedArticles.map((article) {
/// Refresh promotions data final now = DateTime.now();
/// return Promotion(
/// Forces a refresh from the server (when API is available). id: article.id,
/// Updates the cached state with fresh data. title: article.title,
Future<void> refresh() async { description: article.excerpt,
// Set loading state imageUrl: article.imageUrl,
state = const AsyncValue.loading(); startDate: article.publishedDate,
endDate: now.add(const Duration(days: 365)), // Always active
// Fetch fresh data );
state = await AsyncValue.guard(() async { }).toList();
final localDataSource = const HomeLocalDataSourceImpl();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
return await useCase.refresh();
});
}
} }

View File

@@ -10,8 +10,9 @@ part of 'promotions_provider.dart';
// ignore_for_file: type=lint, type=warning // ignore_for_file: type=lint, type=warning
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -25,13 +26,14 @@ part of 'promotions_provider.dart';
/// ); /// );
/// ``` /// ```
@ProviderFor(PromotionsNotifier) @ProviderFor(promotions)
const promotionsProvider = PromotionsNotifierProvider._(); const promotionsProvider = PromotionsProvider._();
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -44,12 +46,20 @@ const promotionsProvider = PromotionsNotifierProvider._();
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
final class PromotionsNotifierProvider
extends $AsyncNotifierProvider<PromotionsNotifier, List<Promotion>> { final class PromotionsProvider
extends
$FunctionalProvider<
AsyncValue<List<Promotion>>,
List<Promotion>,
FutureOr<List<Promotion>>
>
with $FutureModifier<List<Promotion>>, $FutureProvider<List<Promotion>> {
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -62,7 +72,7 @@ final class PromotionsNotifierProvider
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
const PromotionsNotifierProvider._() const PromotionsProvider._()
: super( : super(
from: null, from: null,
argument: null, argument: null,
@@ -74,48 +84,18 @@ final class PromotionsNotifierProvider
); );
@override @override
String debugGetCreateSourceHash() => _$promotionsNotifierHash(); String debugGetCreateSourceHash() => _$promotionsHash();
@$internal @$internal
@override @override
PromotionsNotifier create() => PromotionsNotifier(); $FutureProviderElement<List<Promotion>> $createElement(
} $ProviderPointer pointer,
) => $FutureProviderElement(pointer);
String _$promotionsNotifierHash() =>
r'3cd866c74ba11c6519e9b63521e1757ef117c7a9';
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
///
/// Usage:
/// ```dart
/// // In a ConsumerWidget
/// final promotionsAsync = ref.watch(promotionsProvider);
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
abstract class _$PromotionsNotifier extends $AsyncNotifier<List<Promotion>> {
FutureOr<List<Promotion>> build();
@$mustCallSuper
@override @override
void runBuild() { FutureOr<List<Promotion>> create(Ref ref) {
final created = build(); return promotions(ref);
final ref = this.ref as $Ref<AsyncValue<List<Promotion>>, List<Promotion>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Promotion>>, List<Promotion>>,
AsyncValue<List<Promotion>>,
Object?,
Object?
>;
element.handleValue(ref, created);
} }
} }
String _$promotionsHash() => r'2eac0298d2b84ad5cc50faa6b8a015dbf7b7a1d3';

View File

@@ -11,7 +11,6 @@ import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart';
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart'; import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
@@ -36,8 +35,7 @@ class ProductsPage extends ConsumerWidget {
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
final productsAsync = ref.watch(productsProvider); final productsAsync = ref.watch(productsProvider);
// Preload filter options for better UX when opening filter drawer // Filter options loaded lazily when filter drawer is opened (not here)
ref.watch(productFilterOptionsProvider);
return Scaffold( return Scaffold(
backgroundColor: colorScheme.surfaceContainerLowest, backgroundColor: colorScheme.surfaceContainerLowest,
@@ -105,8 +103,10 @@ class ProductsPage extends ConsumerWidget {
), ),
), ),
// Brand Filter Chips // Brand Filter Chips - only show after products are loaded
const BrandFilterChips(), productsAsync.hasValue
? const BrandFilterChips()
: const SizedBox(height: 48.0),
// Products Grid // Products Grid
Expanded( Expanded(

View File

@@ -86,15 +86,25 @@ class Products extends _$Products {
// Get repository with injected data sources // Get repository with injected data sources
final repository = await ref.watch(productsRepositoryProvider.future); final repository = await ref.watch(productsRepositoryProvider.future);
// Fetch first page of products using unified API // Fetch first page of products
List<Product> products; List<Product> products;
// Build filter parameters from filter drawer // Check if any filters or search are active
final hasFilters = filters.hasActiveFilters;
final hasSearch = searchQuery.isNotEmpty;
if (!hasFilters && !hasSearch) {
// No filters/search: Use simple getAllProducts for faster initial load
products = await repository.getAllProducts(
limitStart: 0,
limitPageLength: pageSize,
);
} else {
// Filters/search active: Use getProductsWithFilters
final List<String>? itemGroups = filters.productLines.isNotEmpty final List<String>? itemGroups = filters.productLines.isNotEmpty
? filters.productLines.toList() ? filters.productLines.toList()
: null; : null;
// Use brands from productFiltersProvider (shared by chips and drawer)
final List<String>? brands = filters.brands.isNotEmpty final List<String>? brands = filters.brands.isNotEmpty
? filters.brands.toList() ? filters.brands.toList()
: null; : null;
@@ -102,24 +112,20 @@ class Products extends _$Products {
// Build item attributes from filter drawer (sizes, surfaces, colors) // Build item attributes from filter drawer (sizes, surfaces, colors)
final List<Map<String, String>> itemAttributes = []; final List<Map<String, String>> itemAttributes = [];
// Add size attributes
for (final size in filters.sizes) { for (final size in filters.sizes) {
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
} }
// Add surface attributes
for (final surface in filters.surfaces) { for (final surface in filters.surfaces) {
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface}); itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
} }
// Add color attributes
for (final color in filters.colors) { for (final color in filters.colors) {
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color}); itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
} }
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null; final String? keyword = hasSearch ? searchQuery : null;
// Use the comprehensive getProductsWithFilters method
products = await repository.getProductsWithFilters( products = await repository.getProductsWithFilters(
limitStart: 0, limitStart: 0,
limitPageLength: pageSize, limitPageLength: pageSize,
@@ -128,6 +134,7 @@ class Products extends _$Products {
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null, itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
searchKeyword: keyword, searchKeyword: keyword,
); );
}
// If we got less than pageSize, there are no more products // If we got less than pageSize, there are no more products
_hasMore = products.length >= pageSize; _hasMore = products.length >= pageSize;
@@ -149,38 +156,45 @@ class Products extends _$Products {
// Calculate pagination parameters // Calculate pagination parameters
final limitStart = _currentPage * pageSize; final limitStart = _currentPage * pageSize;
// Build filter parameters (same logic as build() method) // Check if any filters or search are active
final hasFilters = filters.hasActiveFilters;
final hasSearch = searchQuery.isNotEmpty;
List<Product> newProducts;
if (!hasFilters && !hasSearch) {
// No filters/search: Use simple getAllProducts
newProducts = await repository.getAllProducts(
limitStart: limitStart,
limitPageLength: pageSize,
);
} else {
// Filters/search active: Use getProductsWithFilters
final List<String>? itemGroups = filters.productLines.isNotEmpty final List<String>? itemGroups = filters.productLines.isNotEmpty
? filters.productLines.toList() ? filters.productLines.toList()
: null; : null;
// Use brands from productFiltersProvider (shared by chips and drawer)
final List<String>? brands = filters.brands.isNotEmpty final List<String>? brands = filters.brands.isNotEmpty
? filters.brands.toList() ? filters.brands.toList()
: null; : null;
// Build item attributes from filter drawer (sizes, surfaces, colors)
final List<Map<String, String>> itemAttributes = []; final List<Map<String, String>> itemAttributes = [];
// Add size attributes
for (final size in filters.sizes) { for (final size in filters.sizes) {
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
} }
// Add surface attributes
for (final surface in filters.surfaces) { for (final surface in filters.surfaces) {
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface}); itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
} }
// Add color attributes
for (final color in filters.colors) { for (final color in filters.colors) {
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color}); itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
} }
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null; final String? keyword = hasSearch ? searchQuery : null;
// Fetch next page using unified API newProducts = await repository.getProductsWithFilters(
final newProducts = await repository.getProductsWithFilters(
limitStart: limitStart, limitStart: limitStart,
limitPageLength: pageSize, limitPageLength: pageSize,
itemGroups: itemGroups, itemGroups: itemGroups,
@@ -188,6 +202,7 @@ class Products extends _$Products {
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null, itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
searchKeyword: keyword, searchKeyword: keyword,
); );
}
// If we got less than pageSize, there are no more products // If we got less than pageSize, there are no more products
_hasMore = newProducts.length >= pageSize; _hasMore = newProducts.length >= pageSize;

View File

@@ -228,7 +228,7 @@ final class ProductsProvider
Products create() => Products(); Products create() => Products();
} }
String _$productsHash() => r'6c55b22e75b912281feff3a68f84e488ccb7ab79'; String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4';
/// Products Provider /// Products Provider
/// ///