update products

This commit is contained in:
Phuoc Nguyen
2025-10-15 16:58:20 +07:00
parent f6d2971224
commit 4038f8e8a6
17 changed files with 1172 additions and 314 deletions

869
claude.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../network/dio_client.dart';
part 'dio_client_provider.g.dart';
/// Provider for DioClient singleton
@Riverpod(keepAlive: true)
DioClient dioClient(Ref ref) {
return DioClient();
}

View File

@@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dio_client_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for DioClient singleton
@ProviderFor(dioClient)
const dioClientProvider = DioClientProvider._();
/// Provider for DioClient singleton
final class DioClientProvider
extends $FunctionalProvider<DioClient, DioClient, DioClient>
with $Provider<DioClient> {
/// Provider for DioClient singleton
const DioClientProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dioClientProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dioClientHash();
@$internal
@override
$ProviderElement<DioClient> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
DioClient create(Ref ref) {
return dioClient(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(DioClient value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<DioClient>(value),
);
}
}
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';

View File

@@ -1,3 +1,4 @@
/// Export all core providers
export 'network_info_provider.dart';
export 'sync_status_provider.dart';
export 'dio_client_provider.dart';

View File

@@ -27,7 +27,7 @@ class EmptyState extends StatelessWidget {
children: [
Icon(
icon ?? Icons.inbox_outlined,
size: 80,
size: 50,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 24),

View File

@@ -234,7 +234,7 @@ final class AuthProvider extends $NotifierProvider<Auth, AuthState> {
}
}
String _$authHash() => r'4b053a7691f573316a8957577dd27a3ed73d89be';
String _$authHash() => r'73c9e7b70799eba2904eb6fc65454332d4146a33';
/// Auth state notifier provider

View File

@@ -1,12 +1,19 @@
import '../models/product_model.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
/// Product remote data source using API
abstract class ProductRemoteDataSource {
Future<List<ProductModel>> getAllProducts();
Future<List<ProductModel>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
});
Future<ProductModel> getProductById(String id);
Future<List<ProductModel>> searchProducts(String query);
Future<List<ProductModel>> searchProducts(String query, {int page = 1, int limit = 20});
Future<List<ProductModel>> getProductsByCategory(String categoryId, {int page = 1, int limit = 20});
}
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
@@ -15,25 +22,107 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
ProductRemoteDataSourceImpl(this.client);
@override
Future<List<ProductModel>> getAllProducts() async {
final response = await client.get(ApiConstants.products);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<List<ProductModel>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
if (categoryId != null) {
queryParams['categoryId'] = categoryId;
}
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
final response = await client.get(
ApiConstants.products,
queryParameters: queryParams,
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to fetch products');
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch products: $e');
}
}
@override
Future<ProductModel> getProductById(String id) async {
final response = await client.get(ApiConstants.productById(id));
return ProductModel.fromJson(response.data);
try {
final response = await client.get(ApiConstants.productById(id));
// API returns: { success: true, data: {...product...} }
if (response.data['success'] == true) {
return ProductModel.fromJson(response.data['data']);
} else {
throw ServerException(response.data['message'] ?? 'Product not found');
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch product: $e');
}
}
@override
Future<List<ProductModel>> searchProducts(String query) async {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {'q': query},
);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<List<ProductModel>> searchProducts(String query, {int page = 1, int limit = 20}) async {
try {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {
'q': query,
'page': page,
'limit': limit,
},
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to search products');
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to search products: $e');
}
}
@override
Future<List<ProductModel>> getProductsByCategory(String categoryId, {int page = 1, int limit = 20}) async {
try {
final response = await client.get(
ApiConstants.productsByCategory(categoryId),
queryParameters: {
'page': page,
'limit': limit,
},
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to fetch products by category');
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch products by category: $e');
}
}
}

View File

@@ -86,12 +86,12 @@ class ProductModel extends HiveObject {
return ProductModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
description: json['description'] as String? ?? '',
price: (json['price'] as num).toDouble(),
imageUrl: json['imageUrl'] as String?,
categoryId: json['categoryId'] as String,
stockQuantity: json['stockQuantity'] as int,
isAvailable: json['isAvailable'] as bool,
stockQuantity: json['stockQuantity'] as int? ?? 0,
isAvailable: json['isAvailable'] as bool? ?? true,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);

View File

@@ -0,0 +1,43 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:hive_ce/hive.dart';
import '../datasources/product_local_datasource.dart';
import '../datasources/product_remote_datasource.dart';
import '../repositories/product_repository_impl.dart';
import '../models/product_model.dart';
import '../../domain/repositories/product_repository.dart';
import '../../../../core/providers/providers.dart';
import '../../../../core/constants/storage_constants.dart';
part 'product_providers.g.dart';
/// Provider for product Hive box
@riverpod
Box<ProductModel> productBox(Ref ref) {
return Hive.box<ProductModel>(StorageConstants.productsBox);
}
/// Provider for product local data source
@riverpod
ProductLocalDataSource productLocalDataSource(Ref ref) {
final box = ref.watch(productBoxProvider);
return ProductLocalDataSourceImpl(box);
}
/// Provider for product remote data source
@riverpod
ProductRemoteDataSource productRemoteDataSource(Ref ref) {
final dioClient = ref.watch(dioClientProvider);
return ProductRemoteDataSourceImpl(dioClient);
}
/// Provider for product repository
@riverpod
ProductRepository productRepository(Ref ref) {
final localDataSource = ref.watch(productLocalDataSourceProvider);
final remoteDataSource = ref.watch(productRemoteDataSourceProvider);
return ProductRepositoryImpl(
localDataSource: localDataSource,
remoteDataSource: remoteDataSource,
);
}

View File

@@ -0,0 +1,219 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for product Hive box
@ProviderFor(productBox)
const productBoxProvider = ProductBoxProvider._();
/// Provider for product Hive box
final class ProductBoxProvider
extends
$FunctionalProvider<
Box<ProductModel>,
Box<ProductModel>,
Box<ProductModel>
>
with $Provider<Box<ProductModel>> {
/// Provider for product Hive box
const ProductBoxProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productBoxProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productBoxHash();
@$internal
@override
$ProviderElement<Box<ProductModel>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
Box<ProductModel> create(Ref ref) {
return productBox(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Box<ProductModel> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Box<ProductModel>>(value),
);
}
}
String _$productBoxHash() => r'68cd21ea28cfc716f34daef17849a0393cdb2b80';
/// Provider for product local data source
@ProviderFor(productLocalDataSource)
const productLocalDataSourceProvider = ProductLocalDataSourceProvider._();
/// Provider for product local data source
final class ProductLocalDataSourceProvider
extends
$FunctionalProvider<
ProductLocalDataSource,
ProductLocalDataSource,
ProductLocalDataSource
>
with $Provider<ProductLocalDataSource> {
/// Provider for product local data source
const ProductLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productLocalDataSourceHash();
@$internal
@override
$ProviderElement<ProductLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductLocalDataSource create(Ref ref) {
return productLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductLocalDataSource>(value),
);
}
}
String _$productLocalDataSourceHash() =>
r'ef4673055777e8dc8a8419a80548b319789d99f9';
/// Provider for product remote data source
@ProviderFor(productRemoteDataSource)
const productRemoteDataSourceProvider = ProductRemoteDataSourceProvider._();
/// Provider for product remote data source
final class ProductRemoteDataSourceProvider
extends
$FunctionalProvider<
ProductRemoteDataSource,
ProductRemoteDataSource,
ProductRemoteDataSource
>
with $Provider<ProductRemoteDataSource> {
/// Provider for product remote data source
const ProductRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productRemoteDataSourceHash();
@$internal
@override
$ProviderElement<ProductRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductRemoteDataSource create(Ref ref) {
return productRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductRemoteDataSource>(value),
);
}
}
String _$productRemoteDataSourceHash() =>
r'954798907bb0c9baade27b84eaba612a5dec8f68';
/// Provider for product repository
@ProviderFor(productRepository)
const productRepositoryProvider = ProductRepositoryProvider._();
/// Provider for product repository
final class ProductRepositoryProvider
extends
$FunctionalProvider<
ProductRepository,
ProductRepository,
ProductRepository
>
with $Provider<ProductRepository> {
/// Provider for product repository
const ProductRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productRepositoryHash();
@$internal
@override
$ProviderElement<ProductRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductRepository create(Ref ref) {
return productRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductRepository>(value),
);
}
}
String _$productRepositoryHash() => r'7c5c5b274ce459add6449c29be822ea04503d3dc';

View File

@@ -25,6 +25,13 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
final selectedCategory = ref.watch(product_providers.selectedCategoryProvider);
final productsAsync = ref.watch(productsProvider);
// Debug: Log product loading state
productsAsync.whenOrNull(
data: (products) => debugPrint('Products loaded: ${products.length} items'),
loading: () => debugPrint('Products loading...'),
error: (error, stack) => debugPrint('Products error: $error'),
);
// Get filtered products from the provider
final filteredProducts = productsAsync.when(
data: (products) => products,
@@ -168,12 +175,14 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
),
),
),
body: RefreshIndicator(
onRefresh: () async {
await ref.refresh(productsProvider.future);
await ref.refresh(categoriesProvider.future);
},
child: Column(
body: productsAsync.when(
data: (products) => RefreshIndicator(
onRefresh: () async {
// Force sync with API
await ref.read(productsProvider.notifier).syncProducts();
await ref.refresh(categoriesProvider.future);
},
child: Column(
children: [
// Results count
if (filteredProducts.isNotEmpty)
@@ -194,6 +203,23 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('Error loading products: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(productsProvider),
child: const Text('Retry'),
),
],
),
),
),
);
}

View File

@@ -1,32 +1,92 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/product.dart';
import '../../data/providers/product_providers.dart';
import '../../../../core/providers/providers.dart';
part 'products_provider.g.dart';
/// Provider for products list
/// Provider for products list with API-first approach
@riverpod
class Products extends _$Products {
@override
Future<List<Product>> build() async {
// TODO: Implement with repository
return [];
// API-first: Try to load from API first
final repository = ref.watch(productRepositoryProvider);
final networkInfo = ref.watch(networkInfoProvider);
// Check if online
final isConnected = await networkInfo.isConnected;
if (isConnected) {
// Try API first
try {
final syncResult = await repository.syncProducts();
return syncResult.fold(
(failure) {
// API failed, fallback to cache
print('API failed, falling back to cache: ${failure.message}');
return _loadFromCache();
},
(products) => products,
);
} catch (e) {
// API error, fallback to cache
print('API error, falling back to cache: $e');
return _loadFromCache();
}
} else {
// Offline, load from cache
print('Offline, loading from cache');
return _loadFromCache();
}
}
/// Load products from local cache
Future<List<Product>> _loadFromCache() async {
final repository = ref.read(productRepositoryProvider);
final result = await repository.getAllProducts();
return result.fold(
(failure) {
print('Cache load failed: ${failure.message}');
return <Product>[];
},
(products) => products,
);
}
/// Refresh products from local storage
Future<void> refresh() async {
// TODO: Implement refresh logic
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Fetch products from repository
return [];
final repository = ref.read(productRepositoryProvider);
final result = await repository.getAllProducts();
return result.fold(
(failure) => throw Exception(failure.message),
(products) => products,
);
});
}
/// Sync products from API and update local storage
Future<void> syncProducts() async {
// TODO: Implement sync logic with remote data source
final networkInfo = ref.read(networkInfoProvider);
final isConnected = await networkInfo.isConnected;
if (!isConnected) {
throw Exception('No internet connection');
}
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Sync products from API
return [];
final repository = ref.read(productRepositoryProvider);
final result = await repository.syncProducts();
return result.fold(
(failure) => throw Exception(failure.message),
(products) => products,
);
});
}
}

View File

@@ -8,15 +8,15 @@ part of 'products_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for products list
/// Provider for products list with API-first approach
@ProviderFor(Products)
const productsProvider = ProductsProvider._();
/// Provider for products list
/// Provider for products list with API-first approach
final class ProductsProvider
extends $AsyncNotifierProvider<Products, List<Product>> {
/// Provider for products list
/// Provider for products list with API-first approach
const ProductsProvider._()
: super(
from: null,
@@ -36,9 +36,9 @@ final class ProductsProvider
Products create() => Products();
}
String _$productsHash() => r'9e1d3aaa1d9cf0b4ff03fdfaf4512a7a15336d51';
String _$productsHash() => r'0ff8c2de46bb4b1e29678cc811ec121c9fb4c8eb';
/// Provider for products list
/// Provider for products list with API-first approach
abstract class _$Products extends $AsyncNotifier<List<Product>> {
FutureOr<List<Product>> build();

View File

@@ -2,6 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'app.dart';
import 'core/constants/storage_constants.dart';
import 'features/products/data/models/product_model.dart';
import 'features/categories/data/models/category_model.dart';
import 'features/home/data/models/cart_item_model.dart';
import 'features/home/data/models/transaction_model.dart';
import 'features/settings/data/models/app_settings_model.dart';
/// Main entry point of the application
void main() async {
@@ -12,18 +18,18 @@ void main() async {
await Hive.initFlutter();
// Register Hive adapters
// TODO: Register adapters after running code generation
// Hive.registerAdapter(ProductModelAdapter());
// Hive.registerAdapter(CategoryModelAdapter());
// Hive.registerAdapter(CartItemModelAdapter());
// Hive.registerAdapter(AppSettingsModelAdapter());
Hive.registerAdapter(ProductModelAdapter());
Hive.registerAdapter(CategoryModelAdapter());
Hive.registerAdapter(CartItemModelAdapter());
Hive.registerAdapter(TransactionModelAdapter());
Hive.registerAdapter(AppSettingsModelAdapter());
// Open Hive boxes
// TODO: Open boxes after registering adapters
// await Hive.openBox<ProductModel>(StorageConstants.productsBox);
// await Hive.openBox<CategoryModel>(StorageConstants.categoriesBox);
// await Hive.openBox<CartItemModel>(StorageConstants.cartBox);
// await Hive.openBox<AppSettingsModel>(StorageConstants.settingsBox);
await Hive.openBox<ProductModel>(StorageConstants.productsBox);
await Hive.openBox<CategoryModel>(StorageConstants.categoriesBox);
await Hive.openBox<CartItemModel>(StorageConstants.cartBox);
await Hive.openBox<TransactionModel>(StorageConstants.transactionsBox);
await Hive.openBox<AppSettingsModel>(StorageConstants.settingsBox);
// Run the app with Riverpod (no GetIt needed - using Riverpod for DI)
runApp(