update perf
This commit is contained in:
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$loggingInterceptorHash() =>
|
String _$loggingInterceptorHash() =>
|
||||||
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
|
r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
|
||||||
|
|
||||||
/// Provider for ErrorTransformerInterceptor
|
/// Provider for ErrorTransformerInterceptor
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ final class DioProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4';
|
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
|
||||||
|
|
||||||
/// Provider for DioClient
|
/// Provider for DioClient
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh promotions data
|
return limitedArticles.map((article) {
|
||||||
///
|
final now = DateTime.now();
|
||||||
/// Forces a refresh from the server (when API is available).
|
return Promotion(
|
||||||
/// Updates the cached state with fresh data.
|
id: article.id,
|
||||||
Future<void> refresh() async {
|
title: article.title,
|
||||||
// Set loading state
|
description: article.excerpt,
|
||||||
state = const AsyncValue.loading();
|
imageUrl: article.imageUrl,
|
||||||
|
startDate: article.publishedDate,
|
||||||
// Fetch fresh data
|
endDate: now.add(const Duration(days: 365)), // Always active
|
||||||
state = await AsyncValue.guard(() async {
|
);
|
||||||
final localDataSource = const HomeLocalDataSourceImpl();
|
}).toList();
|
||||||
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
|
|
||||||
final useCase = GetPromotions(repository);
|
|
||||||
|
|
||||||
return await useCase.refresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -86,49 +86,56 @@ 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 List<String>? itemGroups = filters.productLines.isNotEmpty
|
final hasFilters = filters.hasActiveFilters;
|
||||||
? filters.productLines.toList()
|
final hasSearch = searchQuery.isNotEmpty;
|
||||||
: null;
|
|
||||||
|
|
||||||
// Use brands from productFiltersProvider (shared by chips and drawer)
|
if (!hasFilters && !hasSearch) {
|
||||||
final List<String>? brands = filters.brands.isNotEmpty
|
// No filters/search: Use simple getAllProducts for faster initial load
|
||||||
? filters.brands.toList()
|
products = await repository.getAllProducts(
|
||||||
: null;
|
limitStart: 0,
|
||||||
|
limitPageLength: pageSize,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Filters/search active: Use getProductsWithFilters
|
||||||
|
final List<String>? itemGroups = filters.productLines.isNotEmpty
|
||||||
|
? filters.productLines.toList()
|
||||||
|
: null;
|
||||||
|
|
||||||
// Build item attributes from filter drawer (sizes, surfaces, colors)
|
final List<String>? brands = filters.brands.isNotEmpty
|
||||||
final List<Map<String, String>> itemAttributes = [];
|
? filters.brands.toList()
|
||||||
|
: null;
|
||||||
|
|
||||||
// Add size attributes
|
// Build item attributes from filter drawer (sizes, surfaces, colors)
|
||||||
for (final size in filters.sizes) {
|
final List<Map<String, String>> itemAttributes = [];
|
||||||
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
|
||||||
|
for (final size in filters.sizes) {
|
||||||
|
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final surface in filters.surfaces) {
|
||||||
|
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final color in filters.colors) {
|
||||||
|
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? keyword = hasSearch ? searchQuery : null;
|
||||||
|
|
||||||
|
products = await repository.getProductsWithFilters(
|
||||||
|
limitStart: 0,
|
||||||
|
limitPageLength: pageSize,
|
||||||
|
itemGroups: itemGroups,
|
||||||
|
brands: brands,
|
||||||
|
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
||||||
|
searchKeyword: keyword,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add surface attributes
|
|
||||||
for (final surface in filters.surfaces) {
|
|
||||||
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add color attributes
|
|
||||||
for (final color in filters.colors) {
|
|
||||||
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
|
||||||
}
|
|
||||||
|
|
||||||
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
|
|
||||||
|
|
||||||
// Use the comprehensive getProductsWithFilters method
|
|
||||||
products = await repository.getProductsWithFilters(
|
|
||||||
limitStart: 0,
|
|
||||||
limitPageLength: pageSize,
|
|
||||||
itemGroups: itemGroups,
|
|
||||||
brands: brands,
|
|
||||||
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
|
||||||
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;
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
@@ -149,46 +156,54 @@ 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 List<String>? itemGroups = filters.productLines.isNotEmpty
|
final hasFilters = filters.hasActiveFilters;
|
||||||
? filters.productLines.toList()
|
final hasSearch = searchQuery.isNotEmpty;
|
||||||
: null;
|
|
||||||
|
|
||||||
// Use brands from productFiltersProvider (shared by chips and drawer)
|
List<Product> newProducts;
|
||||||
final List<String>? brands = filters.brands.isNotEmpty
|
|
||||||
? filters.brands.toList()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Build item attributes from filter drawer (sizes, surfaces, colors)
|
if (!hasFilters && !hasSearch) {
|
||||||
final List<Map<String, String>> itemAttributes = [];
|
// 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
|
||||||
|
? filters.productLines.toList()
|
||||||
|
: null;
|
||||||
|
|
||||||
// Add size attributes
|
final List<String>? brands = filters.brands.isNotEmpty
|
||||||
for (final size in filters.sizes) {
|
? filters.brands.toList()
|
||||||
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
: null;
|
||||||
|
|
||||||
|
final List<Map<String, String>> itemAttributes = [];
|
||||||
|
|
||||||
|
for (final size in filters.sizes) {
|
||||||
|
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final surface in filters.surfaces) {
|
||||||
|
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final color in filters.colors) {
|
||||||
|
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? keyword = hasSearch ? searchQuery : null;
|
||||||
|
|
||||||
|
newProducts = await repository.getProductsWithFilters(
|
||||||
|
limitStart: limitStart,
|
||||||
|
limitPageLength: pageSize,
|
||||||
|
itemGroups: itemGroups,
|
||||||
|
brands: brands,
|
||||||
|
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
||||||
|
searchKeyword: keyword,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add surface attributes
|
|
||||||
for (final surface in filters.surfaces) {
|
|
||||||
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add color attributes
|
|
||||||
for (final color in filters.colors) {
|
|
||||||
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
|
||||||
}
|
|
||||||
|
|
||||||
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
|
|
||||||
|
|
||||||
// Fetch next page using unified API
|
|
||||||
final newProducts = await repository.getProductsWithFilters(
|
|
||||||
limitStart: limitStart,
|
|
||||||
limitPageLength: pageSize,
|
|
||||||
itemGroups: itemGroups,
|
|
||||||
brands: brands,
|
|
||||||
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user