update database

This commit is contained in:
Phuoc Nguyen
2025-10-24 11:31:48 +07:00
parent f95fa9d0a6
commit c4272f9a21
126 changed files with 23528 additions and 2234 deletions

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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,
);
}
}

View File

@@ -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

View 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,
);
}
}

View 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;
}

View File

@@ -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,
);
}
}

View 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)';
}
}

View File

@@ -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,