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