prodycrts

This commit is contained in:
Phuoc Nguyen
2025-10-20 15:56:34 +07:00
parent e321e9a419
commit f95fa9d0a6
40 changed files with 3123 additions and 447 deletions

View File

@@ -0,0 +1,294 @@
/// Data Source: Products Local Data Source
///
/// Provides mock product and category data for development.
/// In production, this would be replaced with actual API calls.
library;
import 'package:worker/features/products/data/models/category_model.dart';
import 'package:worker/features/products/data/models/product_model.dart';
/// Products Local Data Source Interface
abstract class ProductsLocalDataSource {
Future<List<ProductModel>> getAllProducts();
Future<List<ProductModel>> searchProducts(String query);
Future<List<ProductModel>> getProductsByCategory(String categoryId);
Future<ProductModel> getProductById(String id);
Future<List<CategoryModel>> getCategories();
}
/// Products Local Data Source Implementation
///
/// Provides mock data for products and categories.
/// Simulates async operations with delays.
class ProductsLocalDataSourceImpl implements ProductsLocalDataSource {
const ProductsLocalDataSourceImpl();
/// Mock categories data
static final List<Map<String, dynamic>> _categoriesJson = [
{
'id': 'all',
'name': 'Tất cả',
'description': 'Tất cả sản phẩm',
'icon': '📦',
'order': 0,
},
{
'id': 'floor_tiles',
'name': 'Gạch lát nền',
'description': 'Gạch lát nền cao cấp',
'icon': '🏠',
'order': 1,
},
{
'id': 'wall_tiles',
'name': 'Gạch ốp tường',
'description': 'Gạch ốp tường chất lượng',
'icon': '🧱',
'order': 2,
},
{
'id': 'decorative_tiles',
'name': 'Gạch trang trí',
'description': 'Gạch trang trí nghệ thuật',
'icon': '',
'order': 3,
},
{
'id': 'outdoor_tiles',
'name': 'Gạch ngoài trời',
'description': 'Gạch chống trượt ngoài trời',
'icon': '🌳',
'order': 4,
},
{
'id': 'accessories',
'name': 'Phụ kiện',
'description': 'Phụ kiện xây dựng',
'icon': '🔧',
'order': 5,
},
];
/// Mock products data
static final List<Map<String, dynamic>> _productsJson = [
{
'id': 'prod_001',
'name': 'Gạch men cao cấp 60x60',
'sku': 'GM-60-001',
'description': 'Gạch men bóng kiếng cao cấp, chống trượt, độ bền cao. Phù hợp cho phòng khách, phòng ngủ.',
'price': 450000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1615971677499-5467cbfe1f10?w=400',
'categoryId': 'floor_tiles',
'inStock': true,
'stockQuantity': 150,
'createdAt': '2024-01-15T08:00:00Z',
'salePrice': null,
'brand': 'Eurotile',
},
{
'id': 'prod_002',
'name': 'Gạch granite nhập khẩu',
'sku': 'GR-80-002',
'description': 'Gạch granite nhập khẩu Tây Ban Nha, vân đá tự nhiên, sang trọng. Kích thước 80x80cm.',
'price': 680000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1565183928294-7d22e855a326?w=400',
'categoryId': 'floor_tiles',
'inStock': true,
'stockQuantity': 80,
'createdAt': '2024-01-20T10:30:00Z',
'salePrice': 620000.0,
'brand': 'Vasta Stone',
},
{
'id': 'prod_003',
'name': 'Gạch mosaic trang trí',
'sku': 'MS-30-003',
'description': 'Gạch mosaic thủy tinh màu sắc đa dạng, tạo điểm nhấn cho không gian. Kích thước 30x30cm.',
'price': 320000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1604709177225-055f99402ea3?w=400',
'categoryId': 'decorative_tiles',
'inStock': true,
'stockQuantity': 45,
'createdAt': '2024-02-01T14:15:00Z',
'salePrice': null,
'brand': 'Eurotile',
},
{
'id': 'prod_004',
'name': 'Gạch 3D họa tiết',
'sku': '3D-60-004',
'description': 'Gạch 3D với họa tiết nổi độc đáo, tạo hiệu ứng thị giác ấn tượng cho tường phòng khách.',
'price': 750000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1600585152220-90363fe7e115?w=400',
'categoryId': 'wall_tiles',
'inStock': true,
'stockQuantity': 30,
'createdAt': '2024-02-10T09:00:00Z',
'salePrice': 680000.0,
'brand': 'Vasta Stone',
},
{
'id': 'prod_005',
'name': 'Gạch ceramic chống trượt',
'sku': 'CR-40-005',
'description': 'Gạch ceramic chống trượt cấp độ R11, an toàn cho phòng tắm và ban công. Kích thước 40x40cm.',
'price': 380000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=400',
'categoryId': 'outdoor_tiles',
'inStock': true,
'stockQuantity': 8,
'createdAt': '2024-02-15T11:20:00Z',
'salePrice': null,
'brand': 'Eurotile',
},
{
'id': 'prod_006',
'name': 'Gạch terrazzo đá mài',
'sku': 'TZ-60-006',
'description': 'Gạch terrazzo phong cách retro, đá mài hạt màu, độc đáo và bền đẹp theo thời gian.',
'price': 890000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=400',
'categoryId': 'decorative_tiles',
'inStock': true,
'stockQuantity': 25,
'createdAt': '2024-02-20T15:45:00Z',
'salePrice': 820000.0,
'brand': 'Vasta Stone',
},
{
'id': 'prod_007',
'name': 'Gạch ốp tường bếp',
'sku': 'OT-30-007',
'description': 'Gạch ốp tường nhà bếp, dễ lau chùi, chống thấm tốt. Kích thước 30x60cm.',
'price': 280000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1600047509807-ba8f99d2cdde?w=400',
'categoryId': 'wall_tiles',
'inStock': true,
'stockQuantity': 120,
'createdAt': '2024-03-01T08:30:00Z',
'salePrice': null,
'brand': 'Eurotile',
},
{
'id': 'prod_008',
'name': 'Gạch sân vườn chống rêu',
'sku': 'SV-50-008',
'description': 'Gạch lát sân vườn chống rêu mốc, bền với thời tiết. Kích thước 50x50cm.',
'price': 420000.0,
'unit': '',
'imageUrl': 'https://images.unsplash.com/photo-1600566752355-35792bedcfea?w=400',
'categoryId': 'outdoor_tiles',
'inStock': true,
'stockQuantity': 65,
'createdAt': '2024-03-05T10:00:00Z',
'salePrice': 380000.0,
'brand': 'Vasta Stone',
},
{
'id': 'prod_009',
'name': 'Keo dán gạch chuyên dụng',
'sku': 'ACC-KD-009',
'description': 'Keo dán gạch chất lượng cao, độ bám dính mạnh, chống thấm. Bao 25kg.',
'price': 180000.0,
'unit': 'bao',
'imageUrl': 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=400',
'categoryId': 'accessories',
'inStock': true,
'stockQuantity': 200,
'createdAt': '2024-03-10T13:15:00Z',
'salePrice': null,
'brand': 'Eurotile',
},
{
'id': 'prod_010',
'name': 'Keo chà ron màu',
'sku': 'ACC-KCR-010',
'description': 'Keo chà ron gạch nhiều màu sắc, chống thấm, chống nấm mốc. Bao 5kg.',
'price': 120000.0,
'unit': 'bao',
'imageUrl': 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=400',
'categoryId': 'accessories',
'inStock': true,
'stockQuantity': 150,
'createdAt': '2024-03-15T09:45:00Z',
'salePrice': 99000.0,
'brand': 'Vasta Stone',
},
];
@override
Future<List<ProductModel>> getAllProducts() async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 500));
return _productsJson
.map((json) => ProductModel.fromJson(json))
.toList();
}
@override
Future<List<ProductModel>> searchProducts(String query) async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 300));
final lowercaseQuery = query.toLowerCase();
final filtered = _productsJson.where((product) {
final name = (product['name'] as String).toLowerCase();
final sku = (product['sku'] as String).toLowerCase();
final description = (product['description'] as String).toLowerCase();
return name.contains(lowercaseQuery) ||
sku.contains(lowercaseQuery) ||
description.contains(lowercaseQuery);
}).toList();
return filtered.map((json) => ProductModel.fromJson(json)).toList();
}
@override
Future<List<ProductModel>> getProductsByCategory(String categoryId) async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 400));
if (categoryId == 'all') {
return getAllProducts();
}
final filtered = _productsJson
.where((product) => product['categoryId'] == categoryId)
.toList();
return filtered.map((json) => ProductModel.fromJson(json)).toList();
}
@override
Future<ProductModel> getProductById(String id) async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 200));
final productJson = _productsJson.firstWhere(
(product) => product['id'] == id,
orElse: () => throw Exception('Product not found: $id'),
);
return ProductModel.fromJson(productJson);
}
@override
Future<List<CategoryModel>> getCategories() async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 300));
return _categoriesJson
.map((json) => CategoryModel.fromJson(json))
.toList();
}
}

View File

@@ -0,0 +1,131 @@
/// Data Model: Category
///
/// Data Transfer Object for category information.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/features/products/domain/entities/category.dart';
part 'category_model.g.dart';
/// Category Model
///
/// Used for:
/// - JSON serialization/deserialization
/// - Hive local database storage
/// - Converting to/from domain entity
///
/// Hive Type ID: 12
@HiveType(typeId: 12)
class CategoryModel extends HiveObject {
/// Unique identifier
@HiveField(0)
final String id;
/// Category name
@HiveField(1)
final String name;
/// Category description
@HiveField(2)
final String description;
/// Icon name or emoji
@HiveField(3)
final String icon;
/// Display order
@HiveField(4)
final int order;
CategoryModel({
required this.id,
required this.name,
required this.description,
required this.icon,
required this.order,
});
/// From JSON constructor
factory CategoryModel.fromJson(Map<String, dynamic> json) {
return CategoryModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
icon: json['icon'] as String,
order: json['order'] as int,
);
}
/// To JSON method
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'icon': icon,
'order': order,
};
}
/// Convert to domain entity
Category toEntity() {
return Category(
id: id,
name: name,
description: description,
icon: icon,
order: order,
);
}
/// Create from domain entity
factory CategoryModel.fromEntity(Category entity) {
return CategoryModel(
id: entity.id,
name: entity.name,
description: entity.description,
icon: entity.icon,
order: entity.order,
);
}
/// Copy with method
CategoryModel copyWith({
String? id,
String? name,
String? description,
String? icon,
int? order,
}) {
return CategoryModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
icon: icon ?? this.icon,
order: order ?? this.order,
);
}
@override
String toString() {
return 'CategoryModel(id: $id, name: $name, order: $order)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CategoryModel &&
other.id == id &&
other.name == name &&
other.description == description &&
other.icon == icon &&
other.order == order;
}
@override
int get hashCode {
return Object.hash(id, name, description, icon, order);
}
}

View File

@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'category_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
@override
final typeId = 12;
@override
CategoryModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CategoryModel(
id: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String,
icon: fields[3] as String,
order: (fields[4] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, CategoryModel obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.icon)
..writeByte(4)
..write(obj.order);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CategoryModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,242 @@
/// Data Model: Product
///
/// Data Transfer Object for product information.
/// Handles JSON and Hive serialization/deserialization.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/features/products/domain/entities/product.dart';
part 'product_model.g.dart';
/// Product Model
///
/// Used for:
/// - JSON serialization/deserialization
/// - Hive local database storage
/// - Converting to/from domain entity
///
/// Hive Type ID: 1
@HiveType(typeId: 1)
class ProductModel extends HiveObject {
/// Unique identifier
@HiveField(0)
final String id;
/// Product name
@HiveField(1)
final String name;
/// Product SKU
@HiveField(2)
final String sku;
/// Product description
@HiveField(3)
final String description;
/// Price per unit (VND)
@HiveField(4)
final double price;
/// Unit of measurement
@HiveField(5)
final String unit;
/// Product image URL
@HiveField(6)
final String imageUrl;
/// Category ID
@HiveField(7)
final String categoryId;
/// Stock availability
@HiveField(8)
final bool inStock;
/// Stock quantity
@HiveField(9)
final int stockQuantity;
/// Created date (ISO8601 string)
@HiveField(10)
final String createdAt;
/// Sale price (optional)
@HiveField(11)
final double? salePrice;
/// Brand name (optional)
@HiveField(12)
final String? brand;
ProductModel({
required this.id,
required this.name,
required this.sku,
required this.description,
required this.price,
required this.unit,
required this.imageUrl,
required this.categoryId,
required this.inStock,
required this.stockQuantity,
required this.createdAt,
this.salePrice,
this.brand,
});
/// From JSON constructor
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'] as String,
name: json['name'] as String,
sku: json['sku'] as String,
description: json['description'] as String,
price: (json['price'] as num).toDouble(),
unit: json['unit'] as String,
imageUrl: json['imageUrl'] as String,
categoryId: json['categoryId'] as String,
inStock: json['inStock'] as bool,
stockQuantity: json['stockQuantity'] as int,
createdAt: json['createdAt'] as String,
salePrice: json['salePrice'] != null ? (json['salePrice'] as num).toDouble() : null,
brand: json['brand'] as String?,
);
}
/// To JSON method
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'sku': sku,
'description': description,
'price': price,
'unit': unit,
'imageUrl': imageUrl,
'categoryId': categoryId,
'inStock': inStock,
'stockQuantity': stockQuantity,
'createdAt': createdAt,
'salePrice': salePrice,
'brand': brand,
};
}
/// Convert to domain entity
Product toEntity() {
return Product(
id: id,
name: name,
sku: sku,
description: description,
price: price,
unit: unit,
imageUrl: imageUrl,
categoryId: categoryId,
inStock: inStock,
stockQuantity: stockQuantity,
createdAt: DateTime.parse(createdAt),
salePrice: salePrice,
brand: brand,
);
}
/// Create from domain entity
factory ProductModel.fromEntity(Product entity) {
return ProductModel(
id: entity.id,
name: entity.name,
sku: entity.sku,
description: entity.description,
price: entity.price,
unit: entity.unit,
imageUrl: entity.imageUrl,
categoryId: entity.categoryId,
inStock: entity.inStock,
stockQuantity: entity.stockQuantity,
createdAt: entity.createdAt.toIso8601String(),
salePrice: entity.salePrice,
brand: entity.brand,
);
}
/// Copy with method
ProductModel copyWith({
String? id,
String? name,
String? sku,
String? description,
double? price,
String? unit,
String? imageUrl,
String? categoryId,
bool? inStock,
int? stockQuantity,
String? createdAt,
double? salePrice,
String? brand,
}) {
return ProductModel(
id: id ?? this.id,
name: name ?? this.name,
sku: sku ?? this.sku,
description: description ?? this.description,
price: price ?? this.price,
unit: unit ?? this.unit,
imageUrl: imageUrl ?? this.imageUrl,
categoryId: categoryId ?? this.categoryId,
inStock: inStock ?? this.inStock,
stockQuantity: stockQuantity ?? this.stockQuantity,
createdAt: createdAt ?? this.createdAt,
salePrice: salePrice ?? this.salePrice,
brand: brand ?? this.brand,
);
}
@override
String toString() {
return 'ProductModel(id: $id, name: $name, sku: $sku, price: $price, unit: $unit)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProductModel &&
other.id == id &&
other.name == name &&
other.sku == sku &&
other.description == description &&
other.price == price &&
other.unit == unit &&
other.imageUrl == imageUrl &&
other.categoryId == categoryId &&
other.inStock == inStock &&
other.stockQuantity == stockQuantity &&
other.createdAt == createdAt &&
other.salePrice == salePrice &&
other.brand == brand;
}
@override
int get hashCode {
return Object.hash(
id,
name,
sku,
description,
price,
unit,
imageUrl,
categoryId,
inStock,
stockQuantity,
createdAt,
salePrice,
brand,
);
}
}

View File

@@ -0,0 +1,77 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ProductModelAdapter extends TypeAdapter<ProductModel> {
@override
final typeId = 1;
@override
ProductModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProductModel(
id: fields[0] as String,
name: fields[1] as String,
sku: fields[2] as String,
description: fields[3] as String,
price: (fields[4] as num).toDouble(),
unit: fields[5] as String,
imageUrl: fields[6] as String,
categoryId: fields[7] as String,
inStock: fields[8] as bool,
stockQuantity: (fields[9] as num).toInt(),
createdAt: fields[10] as String,
salePrice: (fields[11] as num?)?.toDouble(),
brand: fields[12] as String?,
);
}
@override
void write(BinaryWriter writer, ProductModel obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.sku)
..writeByte(3)
..write(obj.description)
..writeByte(4)
..write(obj.price)
..writeByte(5)
..write(obj.unit)
..writeByte(6)
..write(obj.imageUrl)
..writeByte(7)
..write(obj.categoryId)
..writeByte(8)
..write(obj.inStock)
..writeByte(9)
..write(obj.stockQuantity)
..writeByte(10)
..write(obj.createdAt)
..writeByte(11)
..write(obj.salePrice)
..writeByte(12)
..write(obj.brand);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProductModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,72 @@
/// Repository Implementation: Products Repository
///
/// Concrete implementation of the products repository interface.
/// Handles data from local datasource and converts to domain entities.
library;
import 'package:worker/features/products/data/datasources/products_local_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';
/// Products Repository Implementation
///
/// Implements the repository interface defined in the domain layer.
/// Coordinates data from local datasource and converts models to entities.
class ProductsRepositoryImpl implements ProductsRepository {
final ProductsLocalDataSource localDataSource;
const ProductsRepositoryImpl({
required this.localDataSource,
});
@override
Future<List<Product>> getAllProducts() async {
try {
final productModels = await localDataSource.getAllProducts();
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get products: $e');
}
}
@override
Future<List<Product>> searchProducts(String query) async {
try {
final productModels = await localDataSource.searchProducts(query);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to search products: $e');
}
}
@override
Future<List<Product>> getProductsByCategory(String categoryId) async {
try {
final productModels = await localDataSource.getProductsByCategory(categoryId);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get products by category: $e');
}
}
@override
Future<Product> getProductById(String id) async {
try {
final productModel = await localDataSource.getProductById(id);
return productModel.toEntity();
} catch (e) {
throw Exception('Failed to get product: $e');
}
}
@override
Future<List<Category>> getCategories() async {
try {
final categoryModels = await localDataSource.getCategories();
return categoryModels.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get categories: $e');
}
}
}