update database
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/products/domain/entities/category.dart';
|
||||
|
||||
part 'category_model.g.dart';
|
||||
@@ -15,8 +16,8 @@ part 'category_model.g.dart';
|
||||
/// - Hive local database storage
|
||||
/// - Converting to/from domain entity
|
||||
///
|
||||
/// Hive Type ID: 12
|
||||
@HiveType(typeId: 12)
|
||||
/// Hive Type ID: 27 (from HiveTypeIds.categoryModel)
|
||||
@HiveType(typeId: HiveTypeIds.categoryModel)
|
||||
class CategoryModel extends HiveObject {
|
||||
/// Unique identifier
|
||||
@HiveField(0)
|
||||
|
||||
@@ -8,7 +8,7 @@ part of 'category_model.dart';
|
||||
|
||||
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
@override
|
||||
final typeId = 12;
|
||||
final typeId = 27;
|
||||
|
||||
@override
|
||||
CategoryModel read(BinaryReader reader) {
|
||||
|
||||
@@ -1,242 +1,294 @@
|
||||
/// Data Model: Product
|
||||
///
|
||||
/// Data Transfer Object for product information.
|
||||
/// Handles JSON and Hive serialization/deserialization.
|
||||
library;
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.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 CE model for caching product data locally.
|
||||
/// Maps to the 'products' table in the database.
|
||||
///
|
||||
/// Hive Type ID: 1
|
||||
@HiveType(typeId: 1)
|
||||
/// Type ID: 2
|
||||
@HiveType(typeId: HiveTypeIds.productModel)
|
||||
class ProductModel extends HiveObject {
|
||||
/// Unique identifier
|
||||
ProductModel({
|
||||
required this.productId,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.basePrice,
|
||||
this.images,
|
||||
this.imageCaptions,
|
||||
this.link360,
|
||||
this.specifications,
|
||||
this.category,
|
||||
this.brand,
|
||||
this.unit,
|
||||
required this.isActive,
|
||||
required this.isFeatured,
|
||||
this.erpnextItemCode,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
/// Product ID (Primary Key)
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
final String productId;
|
||||
|
||||
/// Product name
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
/// Product SKU
|
||||
@HiveField(2)
|
||||
final String sku;
|
||||
|
||||
/// Product description
|
||||
@HiveField(2)
|
||||
final String? description;
|
||||
|
||||
/// Base price
|
||||
@HiveField(3)
|
||||
final String description;
|
||||
final double basePrice;
|
||||
|
||||
/// Price per unit (VND)
|
||||
/// Product images (JSON encoded list of URLs)
|
||||
@HiveField(4)
|
||||
final double price;
|
||||
final String? images;
|
||||
|
||||
/// Unit of measurement
|
||||
/// Image captions (JSON encoded map of image_url -> caption)
|
||||
@HiveField(5)
|
||||
final String unit;
|
||||
final String? imageCaptions;
|
||||
|
||||
/// Product image URL
|
||||
/// 360-degree view link
|
||||
@HiveField(6)
|
||||
final String imageUrl;
|
||||
final String? link360;
|
||||
|
||||
/// Category ID
|
||||
/// Product specifications (JSON encoded)
|
||||
/// Contains: size, material, color, finish, etc.
|
||||
@HiveField(7)
|
||||
final String categoryId;
|
||||
final String? specifications;
|
||||
|
||||
/// Stock availability
|
||||
/// Product category
|
||||
@HiveField(8)
|
||||
final bool inStock;
|
||||
final String? category;
|
||||
|
||||
/// Stock quantity
|
||||
/// Product brand
|
||||
@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,
|
||||
});
|
||||
/// Unit of measurement (m2, box, piece, etc.)
|
||||
@HiveField(10)
|
||||
final String? unit;
|
||||
|
||||
/// From JSON constructor
|
||||
/// Whether product is active
|
||||
@HiveField(11)
|
||||
final bool isActive;
|
||||
|
||||
/// Whether product is featured
|
||||
@HiveField(12)
|
||||
final bool isFeatured;
|
||||
|
||||
/// ERPNext item code for integration
|
||||
@HiveField(13)
|
||||
final String? erpnextItemCode;
|
||||
|
||||
/// Product creation timestamp
|
||||
@HiveField(14)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Last update timestamp
|
||||
@HiveField(15)
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// =========================================================================
|
||||
// JSON SERIALIZATION
|
||||
// =========================================================================
|
||||
|
||||
/// Create ProductModel from JSON
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
id: json['id'] as String,
|
||||
productId: json['product_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,
|
||||
description: json['description'] as String?,
|
||||
basePrice: (json['base_price'] as num).toDouble(),
|
||||
images: json['images'] != null ? jsonEncode(json['images']) : null,
|
||||
imageCaptions: json['image_captions'] != null
|
||||
? jsonEncode(json['image_captions'])
|
||||
: null,
|
||||
link360: json['link_360'] as String?,
|
||||
specifications: json['specifications'] != null
|
||||
? jsonEncode(json['specifications'])
|
||||
: null,
|
||||
category: json['category'] as String?,
|
||||
brand: json['brand'] as String?,
|
||||
unit: json['unit'] as String?,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
isFeatured: json['is_featured'] as bool? ?? false,
|
||||
erpnextItemCode: json['erpnext_item_code'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// To JSON method
|
||||
/// Convert ProductModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'product_id': productId,
|
||||
'name': name,
|
||||
'sku': sku,
|
||||
'description': description,
|
||||
'price': price,
|
||||
'unit': unit,
|
||||
'imageUrl': imageUrl,
|
||||
'categoryId': categoryId,
|
||||
'inStock': inStock,
|
||||
'stockQuantity': stockQuantity,
|
||||
'createdAt': createdAt,
|
||||
'salePrice': salePrice,
|
||||
'base_price': basePrice,
|
||||
'images': images != null ? jsonDecode(images!) : null,
|
||||
'image_captions':
|
||||
imageCaptions != null ? jsonDecode(imageCaptions!) : null,
|
||||
'link_360': link360,
|
||||
'specifications':
|
||||
specifications != null ? jsonDecode(specifications!) : null,
|
||||
'category': category,
|
||||
'brand': brand,
|
||||
'unit': unit,
|
||||
'is_active': isActive,
|
||||
'is_featured': isFeatured,
|
||||
'erpnext_item_code': erpnextItemCode,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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,
|
||||
);
|
||||
// =========================================================================
|
||||
// HELPER METHODS
|
||||
// =========================================================================
|
||||
|
||||
/// Get images as List
|
||||
List<String>? get imagesList {
|
||||
if (images == null) return null;
|
||||
try {
|
||||
final decoded = jsonDecode(images!) as List;
|
||||
return decoded.map((e) => e.toString()).toList();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
);
|
||||
/// Get first image or placeholder
|
||||
String get primaryImage {
|
||||
final imgs = imagesList;
|
||||
if (imgs != null && imgs.isNotEmpty) {
|
||||
return imgs.first;
|
||||
}
|
||||
return ''; // Return empty string, UI should handle placeholder
|
||||
}
|
||||
|
||||
/// Copy with method
|
||||
/// Get image captions as Map
|
||||
Map<String, String>? get imageCaptionsMap {
|
||||
if (imageCaptions == null) return null;
|
||||
try {
|
||||
final decoded = jsonDecode(imageCaptions!) as Map<String, dynamic>;
|
||||
return decoded.map((key, value) => MapEntry(key, value.toString()));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get specifications as Map
|
||||
Map<String, dynamic>? get specificationsMap {
|
||||
if (specifications == null) return null;
|
||||
try {
|
||||
return jsonDecode(specifications!) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get formatted price with currency
|
||||
String get formattedPrice {
|
||||
return '${basePrice.toStringAsFixed(0)}đ';
|
||||
}
|
||||
|
||||
/// Check if product has 360 view
|
||||
bool get has360View => link360 != null && link360!.isNotEmpty;
|
||||
|
||||
// =========================================================================
|
||||
// COPY WITH
|
||||
// =========================================================================
|
||||
|
||||
/// Create a copy with updated fields
|
||||
ProductModel copyWith({
|
||||
String? id,
|
||||
String? productId,
|
||||
String? name,
|
||||
String? sku,
|
||||
String? description,
|
||||
double? price,
|
||||
String? unit,
|
||||
String? imageUrl,
|
||||
String? categoryId,
|
||||
bool? inStock,
|
||||
int? stockQuantity,
|
||||
String? createdAt,
|
||||
double? salePrice,
|
||||
double? basePrice,
|
||||
String? images,
|
||||
String? imageCaptions,
|
||||
String? link360,
|
||||
String? specifications,
|
||||
String? category,
|
||||
String? brand,
|
||||
String? unit,
|
||||
bool? isActive,
|
||||
bool? isFeatured,
|
||||
String? erpnextItemCode,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return ProductModel(
|
||||
id: id ?? this.id,
|
||||
productId: productId ?? this.productId,
|
||||
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,
|
||||
basePrice: basePrice ?? this.basePrice,
|
||||
images: images ?? this.images,
|
||||
imageCaptions: imageCaptions ?? this.imageCaptions,
|
||||
link360: link360 ?? this.link360,
|
||||
specifications: specifications ?? this.specifications,
|
||||
category: category ?? this.category,
|
||||
brand: brand ?? this.brand,
|
||||
unit: unit ?? this.unit,
|
||||
isActive: isActive ?? this.isActive,
|
||||
isFeatured: isFeatured ?? this.isFeatured,
|
||||
erpnextItemCode: erpnextItemCode ?? this.erpnextItemCode,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductModel(id: $id, name: $name, sku: $sku, price: $price, unit: $unit)';
|
||||
return 'ProductModel(productId: $productId, name: $name, price: $basePrice)';
|
||||
}
|
||||
|
||||
@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;
|
||||
return other is ProductModel && other.productId == productId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
id,
|
||||
name,
|
||||
sku,
|
||||
description,
|
||||
price,
|
||||
unit,
|
||||
imageUrl,
|
||||
categoryId,
|
||||
inStock,
|
||||
stockQuantity,
|
||||
createdAt,
|
||||
salePrice,
|
||||
brand,
|
||||
int get hashCode => productId.hashCode;
|
||||
|
||||
// =========================================================================
|
||||
// ENTITY CONVERSION
|
||||
// =========================================================================
|
||||
|
||||
/// Convert ProductModel to Product entity
|
||||
Product toEntity() {
|
||||
return Product(
|
||||
productId: productId,
|
||||
name: name,
|
||||
description: description,
|
||||
basePrice: basePrice,
|
||||
images: imagesList ?? [],
|
||||
imageCaptions: imageCaptionsMap ?? {},
|
||||
link360: link360,
|
||||
specifications: specificationsMap ?? {},
|
||||
category: category,
|
||||
brand: brand,
|
||||
unit: unit,
|
||||
isActive: isActive,
|
||||
isFeatured: isFeatured,
|
||||
erpnextItemCode: erpnextItemCode,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ part of 'product_model.dart';
|
||||
|
||||
class ProductModelAdapter extends TypeAdapter<ProductModel> {
|
||||
@override
|
||||
final typeId = 1;
|
||||
final typeId = 2;
|
||||
|
||||
@override
|
||||
ProductModel read(BinaryReader reader) {
|
||||
@@ -17,52 +17,61 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ProductModel(
|
||||
id: fields[0] as String,
|
||||
productId: 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?,
|
||||
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?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ProductModel obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(16)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..write(obj.productId)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.sku)
|
||||
..writeByte(3)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.basePrice)
|
||||
..writeByte(4)
|
||||
..write(obj.price)
|
||||
..write(obj.images)
|
||||
..writeByte(5)
|
||||
..write(obj.unit)
|
||||
..write(obj.imageCaptions)
|
||||
..writeByte(6)
|
||||
..write(obj.imageUrl)
|
||||
..write(obj.link360)
|
||||
..writeByte(7)
|
||||
..write(obj.categoryId)
|
||||
..write(obj.specifications)
|
||||
..writeByte(8)
|
||||
..write(obj.inStock)
|
||||
..write(obj.category)
|
||||
..writeByte(9)
|
||||
..write(obj.stockQuantity)
|
||||
..write(obj.brand)
|
||||
..writeByte(10)
|
||||
..write(obj.createdAt)
|
||||
..write(obj.unit)
|
||||
..writeByte(11)
|
||||
..write(obj.salePrice)
|
||||
..write(obj.isActive)
|
||||
..writeByte(12)
|
||||
..write(obj.brand);
|
||||
..write(obj.isFeatured)
|
||||
..writeByte(13)
|
||||
..write(obj.erpnextItemCode)
|
||||
..writeByte(14)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(15)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
84
lib/features/products/data/models/stock_level_model.dart
Normal file
84
lib/features/products/data/models/stock_level_model.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
|
||||
part 'stock_level_model.g.dart';
|
||||
|
||||
/// Stock Level Model
|
||||
///
|
||||
/// Hive CE model for caching stock level data locally.
|
||||
/// Maps to the 'stock_levels' table in the database.
|
||||
///
|
||||
/// Type ID: 3
|
||||
@HiveType(typeId: HiveTypeIds.stockLevelModel)
|
||||
class StockLevelModel extends HiveObject {
|
||||
StockLevelModel({
|
||||
required this.productId,
|
||||
required this.availableQty,
|
||||
required this.reservedQty,
|
||||
required this.orderedQty,
|
||||
required this.warehouseCode,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
final String productId;
|
||||
|
||||
@HiveField(1)
|
||||
final double availableQty;
|
||||
|
||||
@HiveField(2)
|
||||
final double reservedQty;
|
||||
|
||||
@HiveField(3)
|
||||
final double orderedQty;
|
||||
|
||||
@HiveField(4)
|
||||
final String warehouseCode;
|
||||
|
||||
@HiveField(5)
|
||||
final DateTime lastUpdated;
|
||||
|
||||
factory StockLevelModel.fromJson(Map<String, dynamic> json) {
|
||||
return StockLevelModel(
|
||||
productId: json['product_id'] as String,
|
||||
availableQty: (json['available_qty'] as num).toDouble(),
|
||||
reservedQty: (json['reserved_qty'] as num).toDouble(),
|
||||
orderedQty: (json['ordered_qty'] as num).toDouble(),
|
||||
warehouseCode: json['warehouse_code'] as String,
|
||||
lastUpdated: DateTime.parse(json['last_updated'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'product_id': productId,
|
||||
'available_qty': availableQty,
|
||||
'reserved_qty': reservedQty,
|
||||
'ordered_qty': orderedQty,
|
||||
'warehouse_code': warehouseCode,
|
||||
'last_updated': lastUpdated.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
double get totalQty => availableQty + reservedQty + orderedQty;
|
||||
bool get inStock => availableQty > 0;
|
||||
|
||||
StockLevelModel copyWith({
|
||||
String? productId,
|
||||
double? availableQty,
|
||||
double? reservedQty,
|
||||
double? orderedQty,
|
||||
String? warehouseCode,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return StockLevelModel(
|
||||
productId: productId ?? this.productId,
|
||||
availableQty: availableQty ?? this.availableQty,
|
||||
reservedQty: reservedQty ?? this.reservedQty,
|
||||
orderedQty: orderedQty ?? this.orderedQty,
|
||||
warehouseCode: warehouseCode ?? this.warehouseCode,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/features/products/data/models/stock_level_model.g.dart
Normal file
56
lib/features/products/data/models/stock_level_model.g.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'stock_level_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class StockLevelModelAdapter extends TypeAdapter<StockLevelModel> {
|
||||
@override
|
||||
final typeId = 3;
|
||||
|
||||
@override
|
||||
StockLevelModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return StockLevelModel(
|
||||
productId: fields[0] as String,
|
||||
availableQty: (fields[1] as num).toDouble(),
|
||||
reservedQty: (fields[2] as num).toDouble(),
|
||||
orderedQty: (fields[3] as num).toDouble(),
|
||||
warehouseCode: fields[4] as String,
|
||||
lastUpdated: fields[5] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, StockLevelModel obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.productId)
|
||||
..writeByte(1)
|
||||
..write(obj.availableQty)
|
||||
..writeByte(2)
|
||||
..write(obj.reservedQty)
|
||||
..writeByte(3)
|
||||
..write(obj.orderedQty)
|
||||
..writeByte(4)
|
||||
..write(obj.warehouseCode)
|
||||
..writeByte(5)
|
||||
..write(obj.lastUpdated);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is StockLevelModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -10,111 +10,155 @@ library;
|
||||
/// Used across all layers but originates in the domain layer.
|
||||
class Product {
|
||||
/// Unique identifier
|
||||
final String id;
|
||||
final String productId;
|
||||
|
||||
/// Product name (Vietnamese)
|
||||
final String name;
|
||||
|
||||
/// Product SKU (Stock Keeping Unit)
|
||||
final String sku;
|
||||
|
||||
/// Product description
|
||||
final String description;
|
||||
final String? description;
|
||||
|
||||
/// Price per unit (VND)
|
||||
final double price;
|
||||
/// Base price per unit (VND)
|
||||
final double basePrice;
|
||||
|
||||
/// Product images (URLs)
|
||||
final List<String> images;
|
||||
|
||||
/// Image captions
|
||||
final Map<String, String> imageCaptions;
|
||||
|
||||
/// 360-degree view link
|
||||
final String? link360;
|
||||
|
||||
/// Product specifications
|
||||
final Map<String, dynamic> specifications;
|
||||
|
||||
/// Category name
|
||||
final String? category;
|
||||
|
||||
/// Brand name
|
||||
final String? brand;
|
||||
|
||||
/// Unit of measurement (e.g., "m²", "viên", "hộp")
|
||||
final String unit;
|
||||
final String? unit;
|
||||
|
||||
/// Product image URL
|
||||
final String imageUrl;
|
||||
/// Product is active
|
||||
final bool isActive;
|
||||
|
||||
/// Category ID
|
||||
final String categoryId;
|
||||
/// Product is featured
|
||||
final bool isFeatured;
|
||||
|
||||
/// Stock availability
|
||||
final bool inStock;
|
||||
|
||||
/// Stock quantity
|
||||
final int stockQuantity;
|
||||
/// ERPNext item code
|
||||
final String? erpnextItemCode;
|
||||
|
||||
/// Created date
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Optional sale price
|
||||
final double? salePrice;
|
||||
|
||||
/// Optional brand name
|
||||
final String? brand;
|
||||
/// Last updated date
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Product({
|
||||
required this.id,
|
||||
required this.productId,
|
||||
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.description,
|
||||
required this.basePrice,
|
||||
required this.images,
|
||||
required this.imageCaptions,
|
||||
this.link360,
|
||||
required this.specifications,
|
||||
this.category,
|
||||
this.brand,
|
||||
this.unit,
|
||||
required this.isActive,
|
||||
required this.isFeatured,
|
||||
this.erpnextItemCode,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Get effective price (sale price if available, otherwise regular price)
|
||||
double get effectivePrice => salePrice ?? price;
|
||||
/// Get primary image URL
|
||||
String? get primaryImage => images.isNotEmpty ? images.first : null;
|
||||
|
||||
/// Alias for primaryImage (used by UI widgets)
|
||||
String get imageUrl => primaryImage ?? '';
|
||||
|
||||
/// Category ID (alias for category field)
|
||||
String? get categoryId => category;
|
||||
|
||||
/// Check if product has 360 view
|
||||
bool get has360View => link360 != null && link360!.isNotEmpty;
|
||||
|
||||
/// Check if product has multiple images
|
||||
bool get hasMultipleImages => images.length > 1;
|
||||
|
||||
/// Check if product is on sale
|
||||
bool get isOnSale => salePrice != null && salePrice! < price;
|
||||
/// TODO: Implement sale price logic when backend supports it
|
||||
bool get isOnSale => false;
|
||||
|
||||
/// Get discount percentage
|
||||
int get discountPercentage {
|
||||
if (!isOnSale) return 0;
|
||||
return (((price - salePrice!) / price) * 100).round();
|
||||
/// Discount percentage
|
||||
/// TODO: Calculate from salePrice when backend supports it
|
||||
int get discountPercentage => 0;
|
||||
|
||||
/// Effective price (considering sales)
|
||||
/// TODO: Use salePrice when backend supports it
|
||||
double get effectivePrice => basePrice;
|
||||
|
||||
/// Check if product is low stock
|
||||
/// TODO: Implement stock tracking when backend supports it
|
||||
bool get isLowStock => false;
|
||||
|
||||
/// Check if product is in stock
|
||||
/// Currently using isActive as proxy
|
||||
bool get inStock => isActive;
|
||||
|
||||
/// Get specification value by key
|
||||
String? getSpecification(String key) {
|
||||
return specifications[key]?.toString();
|
||||
}
|
||||
|
||||
/// Check if stock is low (less than 10 items)
|
||||
bool get isLowStock => inStock && stockQuantity < 10;
|
||||
|
||||
/// Copy with method for creating modified copies
|
||||
Product copyWith({
|
||||
String? id,
|
||||
String? productId,
|
||||
String? name,
|
||||
String? sku,
|
||||
String? description,
|
||||
double? price,
|
||||
String? unit,
|
||||
String? imageUrl,
|
||||
String? categoryId,
|
||||
bool? inStock,
|
||||
int? stockQuantity,
|
||||
DateTime? createdAt,
|
||||
double? salePrice,
|
||||
double? basePrice,
|
||||
List<String>? images,
|
||||
Map<String, String>? imageCaptions,
|
||||
String? link360,
|
||||
Map<String, dynamic>? specifications,
|
||||
String? category,
|
||||
String? brand,
|
||||
String? unit,
|
||||
bool? isActive,
|
||||
bool? isFeatured,
|
||||
String? erpnextItemCode,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return Product(
|
||||
id: id ?? this.id,
|
||||
productId: productId ?? this.productId,
|
||||
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,
|
||||
basePrice: basePrice ?? this.basePrice,
|
||||
images: images ?? this.images,
|
||||
imageCaptions: imageCaptions ?? this.imageCaptions,
|
||||
link360: link360 ?? this.link360,
|
||||
specifications: specifications ?? this.specifications,
|
||||
category: category ?? this.category,
|
||||
brand: brand ?? this.brand,
|
||||
unit: unit ?? this.unit,
|
||||
isActive: isActive ?? this.isActive,
|
||||
isFeatured: isFeatured ?? this.isFeatured,
|
||||
erpnextItemCode: erpnextItemCode ?? this.erpnextItemCode,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Product(id: $id, name: $name, sku: $sku, price: $price, unit: $unit, inStock: $inStock)';
|
||||
return 'Product(productId: $productId, name: $name, basePrice: $basePrice, '
|
||||
'category: $category, isActive: $isActive, isFeatured: $isFeatured)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -122,35 +166,31 @@ class Product {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Product &&
|
||||
other.id == id &&
|
||||
other.productId == productId &&
|
||||
other.name == name &&
|
||||
other.sku == sku &&
|
||||
other.description == description &&
|
||||
other.price == price &&
|
||||
other.basePrice == basePrice &&
|
||||
other.category == category &&
|
||||
other.brand == brand &&
|
||||
other.unit == unit &&
|
||||
other.imageUrl == imageUrl &&
|
||||
other.categoryId == categoryId &&
|
||||
other.inStock == inStock &&
|
||||
other.stockQuantity == stockQuantity &&
|
||||
other.salePrice == salePrice &&
|
||||
other.brand == brand;
|
||||
other.isActive == isActive &&
|
||||
other.isFeatured == isFeatured &&
|
||||
other.erpnextItemCode == erpnextItemCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
id,
|
||||
productId,
|
||||
name,
|
||||
sku,
|
||||
description,
|
||||
price,
|
||||
unit,
|
||||
imageUrl,
|
||||
categoryId,
|
||||
inStock,
|
||||
stockQuantity,
|
||||
salePrice,
|
||||
basePrice,
|
||||
category,
|
||||
brand,
|
||||
unit,
|
||||
isActive,
|
||||
isFeatured,
|
||||
erpnextItemCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
106
lib/features/products/domain/entities/stock_level.dart
Normal file
106
lib/features/products/domain/entities/stock_level.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
/// Domain Entity: Stock Level
|
||||
///
|
||||
/// Represents inventory stock level for a product in a warehouse.
|
||||
library;
|
||||
|
||||
/// Stock Level Entity
|
||||
///
|
||||
/// Contains inventory information for a product:
|
||||
/// - Available quantity
|
||||
/// - Reserved quantity (for pending orders)
|
||||
/// - Ordered quantity (incoming stock)
|
||||
/// - Warehouse location
|
||||
class StockLevel {
|
||||
/// Product ID
|
||||
final String productId;
|
||||
|
||||
/// Available quantity for sale
|
||||
final double availableQty;
|
||||
|
||||
/// Quantity reserved for orders
|
||||
final double reservedQty;
|
||||
|
||||
/// Quantity on order (incoming)
|
||||
final double orderedQty;
|
||||
|
||||
/// Warehouse code
|
||||
final String warehouseCode;
|
||||
|
||||
/// Last update timestamp
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const StockLevel({
|
||||
required this.productId,
|
||||
required this.availableQty,
|
||||
required this.reservedQty,
|
||||
required this.orderedQty,
|
||||
required this.warehouseCode,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
/// Get total quantity (available + reserved + ordered)
|
||||
double get totalQty => availableQty + reservedQty + orderedQty;
|
||||
|
||||
/// Check if product is in stock
|
||||
bool get isInStock => availableQty > 0;
|
||||
|
||||
/// Check if stock is low (less than 10 units)
|
||||
bool get isLowStock => availableQty > 0 && availableQty < 10;
|
||||
|
||||
/// Check if out of stock
|
||||
bool get isOutOfStock => availableQty <= 0;
|
||||
|
||||
/// Get available percentage
|
||||
double get availablePercentage {
|
||||
if (totalQty == 0) return 0;
|
||||
return (availableQty / totalQty) * 100;
|
||||
}
|
||||
|
||||
/// Copy with method for immutability
|
||||
StockLevel copyWith({
|
||||
String? productId,
|
||||
double? availableQty,
|
||||
double? reservedQty,
|
||||
double? orderedQty,
|
||||
String? warehouseCode,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return StockLevel(
|
||||
productId: productId ?? this.productId,
|
||||
availableQty: availableQty ?? this.availableQty,
|
||||
reservedQty: reservedQty ?? this.reservedQty,
|
||||
orderedQty: orderedQty ?? this.orderedQty,
|
||||
warehouseCode: warehouseCode ?? this.warehouseCode,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is StockLevel &&
|
||||
other.productId == productId &&
|
||||
other.availableQty == availableQty &&
|
||||
other.reservedQty == reservedQty &&
|
||||
other.orderedQty == orderedQty &&
|
||||
other.warehouseCode == warehouseCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
productId,
|
||||
availableQty,
|
||||
reservedQty,
|
||||
orderedQty,
|
||||
warehouseCode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StockLevel(productId: $productId, availableQty: $availableQty, '
|
||||
'reservedQty: $reservedQty, orderedQty: $orderedQty, warehouseCode: $warehouseCode)';
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/presentation/providers/categories_provider.dart';
|
||||
@@ -34,6 +35,10 @@ class ProductsPage extends ConsumerWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.white,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text('Sản phẩm', style: TextStyle(color: Colors.black)),
|
||||
elevation: AppBarSpecs.elevation,
|
||||
backgroundColor: AppColors.white,
|
||||
|
||||
Reference in New Issue
Block a user