load products

This commit is contained in:
Phuoc Nguyen
2025-11-10 17:02:26 +07:00
parent 453984cd57
commit b367d405c4
15 changed files with 635 additions and 83 deletions

View File

@@ -0,0 +1,203 @@
/// Data Source: Products Remote Data Source
///
/// Handles fetching product data from the Frappe ERPNext API.
library;
import 'package:dio/dio.dart';
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_service.dart';
import 'package:worker/features/products/data/models/product_model.dart';
/// Products Remote Data Source
///
/// Provides methods to fetch product data from the Frappe ERPNext API.
/// Uses FrappeAuthService for session management.
class ProductsRemoteDataSource {
ProductsRemoteDataSource(this._dioClient, this._frappeAuthService);
final DioClient _dioClient;
final FrappeAuthService _frappeAuthService;
/// Get all products
///
/// Fetches all products from Frappe ERPNext.
/// Returns a list of [ProductModel].
///
/// API endpoint: POST https://land.dbiz.com/api/method/building_material.building_material.api.item.get_list
/// Request body:
/// ```json
/// {
/// "limit_start": 0,
/// "limit_page_length": 0
/// }
/// ```
///
/// Response format:
/// ```json
/// {
/// "message": [
/// {
/// "name": "GIB20 G02",
/// "item_name": "Gạch Eurotile 1200x1200",
/// "description": "Product description...",
/// "standard_rate": 450000.0,
/// "stock_uom": "m²",
/// "image": "/files/image.jpg",
/// "brand": "Eurotile",
/// ...
/// }
/// ]
/// }
/// ```
Future<List<ProductModel>> getAllProducts({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
// Get Frappe session headers
final headers = await _frappeAuthService.getHeaders();
// Build full API URL
const url =
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetItems}';
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
options: Options(headers: headers),
);
if (response.data == null) {
throw Exception('Empty response from server');
}
// Parse the response
final message = response.data!['message'];
if (message == null) {
throw Exception('No message field in response');
}
final productsList = message as List;
return productsList
.map((item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>))
.toList();
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Products endpoint not found');
} else if (e.response?.statusCode == 500) {
throw Exception('Server error while fetching products');
} else if (e.type == DioExceptionType.connectionTimeout) {
throw Exception('Connection timeout while fetching products');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw Exception('Response timeout while fetching products');
} else {
throw Exception('Failed to fetch products: ${e.message}');
}
} catch (e) {
throw Exception('Unexpected error fetching products: $e');
}
}
/// Get product detail by name/code
///
/// Fetches a single product by its item code from Frappe ERPNext.
/// Returns a [ProductModel].
///
/// API endpoint: POST https://land.dbiz.com/api/method/building_material.building_material.api.item.get_detail
/// Request body:
/// ```json
/// {
/// "name": "GIB20 G02"
/// }
/// ```
Future<ProductModel> getProductDetail(String itemCode) async {
try {
// Get Frappe session headers
final headers = await _frappeAuthService.getHeaders();
// Build full API URL
final url =
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetItemDetail}';
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: {
'name': itemCode,
},
options: Options(headers: headers),
);
if (response.data == null) {
throw Exception('Empty response from server');
}
// Parse the response
final message = response.data!['message'];
if (message == null) {
throw Exception('Product not found: $itemCode');
}
return ProductModel.fromFrappeJson(message as Map<String, dynamic>);
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Product not found: $itemCode');
} else if (e.response?.statusCode == 500) {
throw Exception('Server error while fetching product detail');
} else if (e.type == DioExceptionType.connectionTimeout) {
throw Exception('Connection timeout while fetching product detail');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw Exception('Response timeout while fetching product detail');
} else {
throw Exception('Failed to fetch product detail: ${e.message}');
}
} catch (e) {
throw Exception('Unexpected error fetching product detail: $e');
}
}
/// Search products
///
/// Searches products by name or description.
/// For now, we fetch all products and filter locally.
/// In the future, the API might support server-side search.
Future<List<ProductModel>> searchProducts(String query) async {
// For now, fetch all products and filter locally
// TODO: Implement server-side search if API supports it
final allProducts = await getAllProducts();
final lowercaseQuery = query.toLowerCase();
return allProducts.where((product) {
final name = product.name.toLowerCase();
final description = (product.description ?? '').toLowerCase();
final productId = product.productId.toLowerCase();
return name.contains(lowercaseQuery) ||
description.contains(lowercaseQuery) ||
productId.contains(lowercaseQuery);
}).toList();
}
/// Get products by category
///
/// Filters products by category.
/// For now, we fetch all products and filter locally.
/// In the future, the API might support category filtering.
Future<List<ProductModel>> getProductsByCategory(String categoryId) async {
// For now, fetch all products and filter locally
// TODO: Implement server-side category filtering if API supports it
final allProducts = await getAllProducts();
if (categoryId == 'all') {
return allProducts;
}
return allProducts
.where((product) => product.category == categoryId)
.toList();
}
}

View File

@@ -21,8 +21,9 @@ class ProductModel extends HiveObject {
this.description,
required this.basePrice,
this.images,
this.thumbnail,
this.imageCaptions,
this.link360,
this.customLink360,
this.specifications,
this.category,
this.brand,
@@ -54,49 +55,53 @@ class ProductModel extends HiveObject {
@HiveField(4)
final String? images;
/// Image captions (JSON encoded map of image_url -> caption)
/// Thumbnail image URL
@HiveField(5)
final String? thumbnail;
/// Image captions (JSON encoded map of image_url -> caption)
@HiveField(6)
final String? imageCaptions;
/// 360-degree view link
@HiveField(6)
final String? link360;
/// Custom 360-degree view link
@HiveField(7)
final String? customLink360;
/// Product specifications (JSON encoded)
/// Contains: size, material, color, finish, etc.
@HiveField(7)
@HiveField(8)
final String? specifications;
/// Product category
@HiveField(8)
@HiveField(9)
final String? category;
/// Product brand
@HiveField(9)
@HiveField(10)
final String? brand;
/// Unit of measurement (m2, box, piece, etc.)
@HiveField(10)
@HiveField(11)
final String? unit;
/// Whether product is active
@HiveField(11)
@HiveField(12)
final bool isActive;
/// Whether product is featured
@HiveField(12)
@HiveField(13)
final bool isFeatured;
/// ERPNext item code for integration
@HiveField(13)
@HiveField(14)
final String? erpnextItemCode;
/// Product creation timestamp
@HiveField(14)
@HiveField(15)
final DateTime createdAt;
/// Last update timestamp
@HiveField(15)
@HiveField(16)
final DateTime? updatedAt;
// =========================================================================
@@ -111,10 +116,11 @@ class ProductModel extends HiveObject {
description: json['description'] as String?,
basePrice: (json['base_price'] as num).toDouble(),
images: json['images'] != null ? jsonEncode(json['images']) : null,
thumbnail: json['thumbnail'] as String?,
imageCaptions: json['image_captions'] != null
? jsonEncode(json['image_captions'])
: null,
link360: json['link_360'] as String?,
customLink360: json['custom_link_360'] as String?,
specifications: json['specifications'] != null
? jsonEncode(json['specifications'])
: null,
@@ -131,6 +137,79 @@ class ProductModel extends HiveObject {
);
}
/// Create ProductModel from Frappe ERPNext API JSON
///
/// Maps Frappe Item doctype fields to our ProductModel structure.
/// Frappe fields:
/// - name: Item code (e.g., "GIB20 G02")
/// - item_name: Display name
/// - description: Product description
/// - standard_rate: Price
/// - stock_uom: Unit of measurement
/// - image: Image path
/// - thumbnail: Thumbnail image path
/// - brand: Brand name
/// - item_group: Category/group
/// - custom_link_360: Custom 360 view link
factory ProductModel.fromFrappeJson(Map<String, dynamic> json) {
// Handle image - prepend base URL if needed
String? imageUrl;
if (json['image'] != null && (json['image'] as String).isNotEmpty) {
final imagePath = json['image'] as String;
if (imagePath.startsWith('/')) {
imageUrl = 'https://land.dbiz.com$imagePath';
} else if (imagePath.startsWith('http')) {
imageUrl = imagePath;
} else {
imageUrl = 'https://land.dbiz.com/$imagePath';
}
}
// Handle thumbnail - prepend base URL if needed
String? thumbnailUrl;
if (json['thumbnail'] != null && (json['thumbnail'] as String).isNotEmpty) {
final thumbnailPath = json['thumbnail'] as String;
if (thumbnailPath.startsWith('/')) {
thumbnailUrl = 'https://land.dbiz.com$thumbnailPath';
} else if (thumbnailPath.startsWith('http')) {
thumbnailUrl = thumbnailPath;
} else {
thumbnailUrl = 'https://land.dbiz.com/$thumbnailPath';
}
}
// Convert single image to list format
final imagesList = imageUrl != null ? [imageUrl] : [];
final now = DateTime.now();
return ProductModel(
productId: json['name'] as String, // Item code
name: json['item_name'] as String? ?? json['name'] as String,
description: json['description'] as String?,
basePrice: (json['standard_rate'] as num?)?.toDouble() ?? 0.0,
images: imagesList.isNotEmpty ? jsonEncode(imagesList) : null,
thumbnail: thumbnailUrl,
imageCaptions: null, // Not provided by API
customLink360: json['custom_link_360'] as String?,
specifications: json['specifications'] != null
? jsonEncode(json['specifications'])
: null,
category: json['item_group'] as String?, // Frappe uses item_group
brand: json['brand'] as String?,
unit: json['stock_uom'] as String? ?? '',
isActive: (json['disabled'] as int?) == 0, // Frappe uses 'disabled' field
isFeatured: false, // Not provided by API, default to false
erpnextItemCode: json['name'] as String, // Store item code for reference
createdAt: json['creation'] != null
? DateTime.tryParse(json['creation'] as String) ?? now
: now,
updatedAt: json['modified'] != null
? DateTime.tryParse(json['modified'] as String)
: null,
);
}
/// Convert ProductModel to JSON
Map<String, dynamic> toJson() {
return {
@@ -139,10 +218,11 @@ class ProductModel extends HiveObject {
'description': description,
'base_price': basePrice,
'images': images != null ? jsonDecode(images!) : null,
'thumbnail': thumbnail,
'image_captions': imageCaptions != null
? jsonDecode(imageCaptions!)
: null,
'link_360': link360,
'custom_link_360': customLink360,
'specifications': specifications != null
? jsonDecode(specifications!)
: null,
@@ -208,7 +288,7 @@ class ProductModel extends HiveObject {
}
/// Check if product has 360 view
bool get has360View => link360 != null && link360!.isNotEmpty;
bool get has360View => customLink360 != null && customLink360!.isNotEmpty;
// =========================================================================
// COPY WITH
@@ -221,8 +301,9 @@ class ProductModel extends HiveObject {
String? description,
double? basePrice,
String? images,
String? thumbnail,
String? imageCaptions,
String? link360,
String? customLink360,
String? specifications,
String? category,
String? brand,
@@ -239,8 +320,9 @@ class ProductModel extends HiveObject {
description: description ?? this.description,
basePrice: basePrice ?? this.basePrice,
images: images ?? this.images,
thumbnail: thumbnail ?? this.thumbnail,
imageCaptions: imageCaptions ?? this.imageCaptions,
link360: link360 ?? this.link360,
customLink360: customLink360 ?? this.customLink360,
specifications: specifications ?? this.specifications,
category: category ?? this.category,
brand: brand ?? this.brand,
@@ -280,8 +362,9 @@ class ProductModel extends HiveObject {
description: description,
basePrice: basePrice,
images: imagesList ?? [],
thumbnail: thumbnail,
imageCaptions: imageCaptionsMap ?? {},
link360: link360,
customLink360: customLink360,
specifications: specificationsMap ?? {},
category: category,
brand: brand,

View File

@@ -22,24 +22,25 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
description: fields[2] as String?,
basePrice: (fields[3] as num).toDouble(),
images: fields[4] as String?,
imageCaptions: fields[5] as String?,
link360: fields[6] as String?,
specifications: fields[7] as String?,
category: fields[8] as String?,
brand: fields[9] as String?,
unit: fields[10] as String?,
isActive: fields[11] as bool,
isFeatured: fields[12] as bool,
erpnextItemCode: fields[13] as String?,
createdAt: fields[14] as DateTime,
updatedAt: fields[15] as DateTime?,
thumbnail: fields[5] as String?,
imageCaptions: fields[6] as String?,
customLink360: fields[7] as String?,
specifications: fields[8] as String?,
category: fields[9] as String?,
brand: fields[10] as String?,
unit: fields[11] as String?,
isActive: fields[12] as bool,
isFeatured: fields[13] as bool,
erpnextItemCode: fields[14] as String?,
createdAt: fields[15] as DateTime,
updatedAt: fields[16] as DateTime?,
);
}
@override
void write(BinaryWriter writer, ProductModel obj) {
writer
..writeByte(16)
..writeByte(17)
..writeByte(0)
..write(obj.productId)
..writeByte(1)
@@ -51,26 +52,28 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
..writeByte(4)
..write(obj.images)
..writeByte(5)
..write(obj.imageCaptions)
..write(obj.thumbnail)
..writeByte(6)
..write(obj.link360)
..write(obj.imageCaptions)
..writeByte(7)
..write(obj.specifications)
..write(obj.customLink360)
..writeByte(8)
..write(obj.category)
..write(obj.specifications)
..writeByte(9)
..write(obj.brand)
..write(obj.category)
..writeByte(10)
..write(obj.unit)
..write(obj.brand)
..writeByte(11)
..write(obj.isActive)
..write(obj.unit)
..writeByte(12)
..write(obj.isFeatured)
..write(obj.isActive)
..writeByte(13)
..write(obj.erpnextItemCode)
..write(obj.isFeatured)
..writeByte(14)
..write(obj.createdAt)
..write(obj.erpnextItemCode)
..writeByte(15)
..write(obj.createdAt)
..writeByte(16)
..write(obj.updatedAt);
}

View File

@@ -1,10 +1,11 @@
/// Repository Implementation: Products Repository
///
/// Concrete implementation of the products repository interface.
/// Handles data from local datasource and converts to domain entities.
/// Handles data from remote datasource (Frappe API) and converts to domain entities.
library;
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
import 'package:worker/features/products/data/datasources/products_remote_datasource.dart';
import 'package:worker/features/products/domain/entities/category.dart';
import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/features/products/domain/repositories/products_repository.dart';
@@ -12,61 +13,77 @@ import 'package:worker/features/products/domain/repositories/products_repository
/// Products Repository Implementation
///
/// Implements the repository interface defined in the domain layer.
/// Coordinates data from local datasource and converts models to entities.
/// Uses remote datasource (Frappe API) for fetching products.
/// Local datasource is kept for categories (mock data).
class ProductsRepositoryImpl implements ProductsRepository {
final ProductsLocalDataSource localDataSource;
final ProductsRemoteDataSource remoteDataSource;
const ProductsRepositoryImpl({required this.localDataSource});
const ProductsRepositoryImpl({
required this.localDataSource,
required this.remoteDataSource,
});
@override
Future<List<Product>> getAllProducts() async {
try {
final productModels = await localDataSource.getAllProducts();
// Fetch from Frappe API
final productModels = await remoteDataSource.getAllProducts();
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get products: $e');
print('[ProductsRepository] Error getting products: $e');
rethrow; // Re-throw to let providers handle the error
}
}
@override
Future<List<Product>> searchProducts(String query) async {
try {
final productModels = await localDataSource.searchProducts(query);
// Search via remote API
final productModels = await remoteDataSource.searchProducts(query);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to search products: $e');
print('[ProductsRepository] Error searching products: $e');
rethrow;
}
}
@override
Future<List<Product>> getProductsByCategory(String categoryId) async {
try {
final productModels = await localDataSource.getProductsByCategory(
// Filter by category via remote API
final productModels = await remoteDataSource.getProductsByCategory(
categoryId,
);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get products by category: $e');
print('[ProductsRepository] Error getting products by category: $e');
rethrow;
}
}
@override
Future<Product> getProductById(String id) async {
try {
final productModel = await localDataSource.getProductById(id);
// Fetch product detail from Frappe API
final productModel = await remoteDataSource.getProductDetail(id);
return productModel.toEntity();
} catch (e) {
throw Exception('Failed to get product: $e');
print('[ProductsRepository] Error getting product: $e');
rethrow;
}
}
@override
Future<List<Category>> getCategories() async {
try {
// For now, use local mock categories
// TODO: Fetch categories from Frappe API if available
final categoryModels = await localDataSource.getCategories();
return categoryModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get categories: $e');
print('[ProductsRepository] Error getting categories: $e');
rethrow;
}
}
}

View File

@@ -24,11 +24,14 @@ class Product {
/// Product images (URLs)
final List<String> images;
/// Thumbnail image URL
final String? thumbnail;
/// Image captions
final Map<String, String> imageCaptions;
/// 360-degree view link
final String? link360;
/// Custom 360-degree view link
final String? customLink360;
/// Product specifications
final Map<String, dynamic> specifications;
@@ -63,8 +66,9 @@ class Product {
this.description,
required this.basePrice,
required this.images,
this.thumbnail,
required this.imageCaptions,
this.link360,
this.customLink360,
required this.specifications,
this.category,
this.brand,
@@ -86,7 +90,7 @@ class Product {
String? get categoryId => category;
/// Check if product has 360 view
bool get has360View => link360 != null && link360!.isNotEmpty;
bool get has360View => customLink360 != null && customLink360!.isNotEmpty;
/// Check if product has multiple images
bool get hasMultipleImages => images.length > 1;
@@ -123,8 +127,9 @@ class Product {
String? description,
double? basePrice,
List<String>? images,
String? thumbnail,
Map<String, String>? imageCaptions,
String? link360,
String? customLink360,
Map<String, dynamic>? specifications,
String? category,
String? brand,
@@ -141,8 +146,9 @@ class Product {
description: description ?? this.description,
basePrice: basePrice ?? this.basePrice,
images: images ?? this.images,
thumbnail: thumbnail ?? this.thumbnail,
imageCaptions: imageCaptions ?? this.imageCaptions,
link360: link360 ?? this.link360,
customLink360: customLink360 ?? this.customLink360,
specifications: specifications ?? this.specifications,
category: category ?? this.category,
brand: brand ?? this.brand,

View File

@@ -4,10 +4,9 @@
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
import 'package:worker/features/products/data/repositories/products_repository_impl.dart';
import 'package:worker/features/products/domain/entities/category.dart';
import 'package:worker/features/products/domain/usecases/get_categories.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
part 'categories_provider.g.dart';
@@ -28,8 +27,8 @@ part 'categories_provider.g.dart';
/// ```
@riverpod
Future<List<Category>> categories(Ref ref) async {
final localDataSource = const ProductsLocalDataSourceImpl();
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
// Get products repository with injected dependencies
final repository = await ref.watch(productsRepositoryProvider.future);
final useCase = GetCategories(repository);
return await useCase();

View File

@@ -92,4 +92,4 @@ final class CategoriesProvider
}
}
String _$categoriesHash() => r'6de35d3271d6d6572d9cdf5ed68edd26036115fc';
String _$categoriesHash() => r'811c668d2624bbc198bc7c563ed14c7d9ffc390b';

View File

@@ -2,12 +2,17 @@
///
/// Manages the state of products data using Riverpod.
/// Provides filtered products based on category and search query.
/// Fetches data from Frappe ERPNext API via remote data source.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_provider.dart';
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
import 'package:worker/features/products/data/datasources/products_remote_datasource.dart';
import 'package:worker/features/products/data/repositories/products_repository_impl.dart';
import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/features/products/domain/repositories/products_repository.dart';
import 'package:worker/features/products/domain/usecases/get_products.dart';
import 'package:worker/features/products/domain/usecases/search_products.dart';
import 'package:worker/features/products/presentation/providers/selected_category_provider.dart';
@@ -15,10 +20,36 @@ import 'package:worker/features/products/presentation/providers/search_query_pro
part 'products_provider.g.dart';
/// Products Local DataSource Provider
@riverpod
ProductsLocalDataSource productsLocalDataSource(Ref ref) {
return const ProductsLocalDataSourceImpl();
}
/// Products Remote DataSource Provider
@riverpod
Future<ProductsRemoteDataSource> productsRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
final frappeAuthService = ref.watch(frappeAuthServiceProvider);
return ProductsRemoteDataSource(dioClient, frappeAuthService);
}
/// Products Repository Provider
@riverpod
Future<ProductsRepository> productsRepository(Ref ref) async {
final localDataSource = ref.watch(productsLocalDataSourceProvider);
final remoteDataSource = await ref.watch(productsRemoteDataSourceProvider.future);
return ProductsRepositoryImpl(
localDataSource: localDataSource,
remoteDataSource: remoteDataSource,
);
}
/// Products Provider
///
/// Fetches and filters products based on selected category and search query.
/// Automatically updates when category or search query changes.
/// Data is fetched from Frappe ERPNext API.
///
/// Usage:
/// ```dart
@@ -38,9 +69,8 @@ class Products extends _$Products {
final selectedCategory = ref.watch(selectedCategoryProvider);
final searchQuery = ref.watch(searchQueryProvider);
// Initialize dependencies
final localDataSource = const ProductsLocalDataSourceImpl();
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
// Get repository with injected data sources
final repository = await ref.watch(productsRepositoryProvider.future);
// Apply filters
List<Product> products;
@@ -78,10 +108,10 @@ class Products extends _$Products {
///
/// Provides all products without any filtering.
/// Useful for product selection dialogs, etc.
/// Fetches from Frappe ERPNext API.
@riverpod
Future<List<Product>> allProducts(Ref ref) async {
final localDataSource = const ProductsLocalDataSourceImpl();
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
final repository = await ref.watch(productsRepositoryProvider.future);
final getProductsUseCase = GetProducts(repository);
return await getProductsUseCase();

View File

@@ -8,10 +8,158 @@ part of 'products_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Products Local DataSource Provider
@ProviderFor(productsLocalDataSource)
const productsLocalDataSourceProvider = ProductsLocalDataSourceProvider._();
/// Products Local DataSource Provider
final class ProductsLocalDataSourceProvider
extends
$FunctionalProvider<
ProductsLocalDataSource,
ProductsLocalDataSource,
ProductsLocalDataSource
>
with $Provider<ProductsLocalDataSource> {
/// Products Local DataSource Provider
const ProductsLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productsLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productsLocalDataSourceHash();
@$internal
@override
$ProviderElement<ProductsLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductsLocalDataSource create(Ref ref) {
return productsLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductsLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductsLocalDataSource>(value),
);
}
}
String _$productsLocalDataSourceHash() =>
r'c9f87b0affb7b86b890c38175b2b5ff328b7cfa4';
/// Products Remote DataSource Provider
@ProviderFor(productsRemoteDataSource)
const productsRemoteDataSourceProvider = ProductsRemoteDataSourceProvider._();
/// Products Remote DataSource Provider
final class ProductsRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<ProductsRemoteDataSource>,
ProductsRemoteDataSource,
FutureOr<ProductsRemoteDataSource>
>
with
$FutureModifier<ProductsRemoteDataSource>,
$FutureProvider<ProductsRemoteDataSource> {
/// Products Remote DataSource Provider
const ProductsRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productsRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productsRemoteDataSourceHash();
@$internal
@override
$FutureProviderElement<ProductsRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<ProductsRemoteDataSource> create(Ref ref) {
return productsRemoteDataSource(ref);
}
}
String _$productsRemoteDataSourceHash() =>
r'4f08cec50d95b05954ca18a7b04d5f09d1ffd059';
/// Products Repository Provider
@ProviderFor(productsRepository)
const productsRepositoryProvider = ProductsRepositoryProvider._();
/// Products Repository Provider
final class ProductsRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<ProductsRepository>,
ProductsRepository,
FutureOr<ProductsRepository>
>
with
$FutureModifier<ProductsRepository>,
$FutureProvider<ProductsRepository> {
/// Products Repository Provider
const ProductsRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productsRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productsRepositoryHash();
@$internal
@override
$FutureProviderElement<ProductsRepository> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<ProductsRepository> create(Ref ref) {
return productsRepository(ref);
}
}
String _$productsRepositoryHash() =>
r'f1ddd3ec17bbbb70e87a8d47e7c58f681c80c2ae';
/// Products Provider
///
/// Fetches and filters products based on selected category and search query.
/// Automatically updates when category or search query changes.
/// Data is fetched from Frappe ERPNext API.
///
/// Usage:
/// ```dart
@@ -31,6 +179,7 @@ const productsProvider = ProductsProvider._();
///
/// Fetches and filters products based on selected category and search query.
/// Automatically updates when category or search query changes.
/// Data is fetched from Frappe ERPNext API.
///
/// Usage:
/// ```dart
@@ -48,6 +197,7 @@ final class ProductsProvider
///
/// Fetches and filters products based on selected category and search query.
/// Automatically updates when category or search query changes.
/// Data is fetched from Frappe ERPNext API.
///
/// Usage:
/// ```dart
@@ -78,12 +228,13 @@ final class ProductsProvider
Products create() => Products();
}
String _$productsHash() => r'0f1b32d2c14b9d8d600ffb0270f54d32af753e1f';
String _$productsHash() => r'b892402a88484d301cdabd1fde5822ddd29538bf';
/// Products Provider
///
/// Fetches and filters products based on selected category and search query.
/// Automatically updates when category or search query changes.
/// Data is fetched from Frappe ERPNext API.
///
/// Usage:
/// ```dart
@@ -119,6 +270,7 @@ abstract class _$Products extends $AsyncNotifier<List<Product>> {
///
/// Provides all products without any filtering.
/// Useful for product selection dialogs, etc.
/// Fetches from Frappe ERPNext API.
@ProviderFor(allProducts)
const allProductsProvider = AllProductsProvider._();
@@ -127,6 +279,7 @@ const allProductsProvider = AllProductsProvider._();
///
/// Provides all products without any filtering.
/// Useful for product selection dialogs, etc.
/// Fetches from Frappe ERPNext API.
final class AllProductsProvider
extends
@@ -140,6 +293,7 @@ final class AllProductsProvider
///
/// Provides all products without any filtering.
/// Useful for product selection dialogs, etc.
/// Fetches from Frappe ERPNext API.
const AllProductsProvider._()
: super(
from: null,
@@ -166,4 +320,4 @@ final class AllProductsProvider
}
}
String _$allProductsHash() => r'a02e989ad36e644d9b62e681b3ced88e10e4d4c3';
String _$allProductsHash() => r'402d7c6e8d119c7c7eab5e696fb8163831259def';

View File

@@ -61,7 +61,7 @@ class ProductCard extends ConsumerWidget {
top: Radius.circular(ProductCardSpecs.borderRadius),
),
child: CachedNetworkImage(
imageUrl: product.imageUrl,
imageUrl: product.thumbnail ?? '',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,

View File

@@ -48,7 +48,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
}
void _open360View() {
if (widget.product.link360 != null) {
if (widget.product.customLink360 != null) {
// TODO: Open in browser
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(