load products
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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? ?? 'm²',
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user