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

@@ -0,0 +1,45 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'quote_item_model.g.dart';
@HiveType(typeId: HiveTypeIds.quoteItemModel)
class QuoteItemModel extends HiveObject {
QuoteItemModel({required this.quoteItemId, required this.quoteId, required this.productId, required this.quantity, required this.originalPrice, required this.negotiatedPrice, required this.discountPercent, required this.subtotal, this.notes});
@HiveField(0) final String quoteItemId;
@HiveField(1) final String quoteId;
@HiveField(2) final String productId;
@HiveField(3) final double quantity;
@HiveField(4) final double originalPrice;
@HiveField(5) final double negotiatedPrice;
@HiveField(6) final double discountPercent;
@HiveField(7) final double subtotal;
@HiveField(8) final String? notes;
factory QuoteItemModel.fromJson(Map<String, dynamic> json) => QuoteItemModel(
quoteItemId: json['quote_item_id'] as String,
quoteId: json['quote_id'] as String,
productId: json['product_id'] as String,
quantity: (json['quantity'] as num).toDouble(),
originalPrice: (json['original_price'] as num).toDouble(),
negotiatedPrice: (json['negotiated_price'] as num).toDouble(),
discountPercent: (json['discount_percent'] as num).toDouble(),
subtotal: (json['subtotal'] as num).toDouble(),
notes: json['notes'] as String?,
);
Map<String, dynamic> toJson() => {
'quote_item_id': quoteItemId,
'quote_id': quoteId,
'product_id': productId,
'quantity': quantity,
'original_price': originalPrice,
'negotiated_price': negotiatedPrice,
'discount_percent': discountPercent,
'subtotal': subtotal,
'notes': notes,
};
double get totalDiscount => originalPrice * quantity - subtotal;
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quote_item_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class QuoteItemModelAdapter extends TypeAdapter<QuoteItemModel> {
@override
final typeId = 17;
@override
QuoteItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return QuoteItemModel(
quoteItemId: fields[0] as String,
quoteId: fields[1] as String,
productId: fields[2] as String,
quantity: (fields[3] as num).toDouble(),
originalPrice: (fields[4] as num).toDouble(),
negotiatedPrice: (fields[5] as num).toDouble(),
discountPercent: (fields[6] as num).toDouble(),
subtotal: (fields[7] as num).toDouble(),
notes: fields[8] as String?,
);
}
@override
void write(BinaryWriter writer, QuoteItemModel obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.quoteItemId)
..writeByte(1)
..write(obj.quoteId)
..writeByte(2)
..write(obj.productId)
..writeByte(3)
..write(obj.quantity)
..writeByte(4)
..write(obj.originalPrice)
..writeByte(5)
..write(obj.negotiatedPrice)
..writeByte(6)
..write(obj.discountPercent)
..writeByte(7)
..write(obj.subtotal)
..writeByte(8)
..write(obj.notes);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is QuoteItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'quote_model.g.dart';
@HiveType(typeId: HiveTypeIds.quoteModel)
class QuoteModel extends HiveObject {
QuoteModel({required this.quoteId, required this.quoteNumber, required this.userId, required this.status, required this.totalAmount, required this.discountAmount, required this.finalAmount, this.projectName, this.deliveryAddress, this.paymentTerms, this.notes, this.validUntil, this.convertedOrderId, this.erpnextQuotation, required this.createdAt, this.updatedAt});
@HiveField(0) final String quoteId;
@HiveField(1) final String quoteNumber;
@HiveField(2) final String userId;
@HiveField(3) final QuoteStatus status;
@HiveField(4) final double totalAmount;
@HiveField(5) final double discountAmount;
@HiveField(6) final double finalAmount;
@HiveField(7) final String? projectName;
@HiveField(8) final String? deliveryAddress;
@HiveField(9) final String? paymentTerms;
@HiveField(10) final String? notes;
@HiveField(11) final DateTime? validUntil;
@HiveField(12) final String? convertedOrderId;
@HiveField(13) final String? erpnextQuotation;
@HiveField(14) final DateTime createdAt;
@HiveField(15) final DateTime? updatedAt;
factory QuoteModel.fromJson(Map<String, dynamic> json) => QuoteModel(
quoteId: json['quote_id'] as String,
quoteNumber: json['quote_number'] as String,
userId: json['user_id'] as String,
status: QuoteStatus.values.firstWhere((e) => e.name == json['status']),
totalAmount: (json['total_amount'] as num).toDouble(),
discountAmount: (json['discount_amount'] as num).toDouble(),
finalAmount: (json['final_amount'] as num).toDouble(),
projectName: json['project_name'] as String?,
deliveryAddress: json['delivery_address'] != null ? jsonEncode(json['delivery_address']) : null,
paymentTerms: json['payment_terms'] as String?,
notes: json['notes'] as String?,
validUntil: json['valid_until'] != null ? DateTime.parse(json['valid_until']?.toString() ?? '') : null,
convertedOrderId: json['converted_order_id'] as String?,
erpnextQuotation: json['erpnext_quotation'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'quote_id': quoteId,
'quote_number': quoteNumber,
'user_id': userId,
'status': status.name,
'total_amount': totalAmount,
'discount_amount': discountAmount,
'final_amount': finalAmount,
'project_name': projectName,
'delivery_address': deliveryAddress != null ? jsonDecode(deliveryAddress!) : null,
'payment_terms': paymentTerms,
'notes': notes,
'valid_until': validUntil?.toIso8601String(),
'converted_order_id': convertedOrderId,
'erpnext_quotation': erpnextQuotation,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
bool get isExpired => validUntil != null && DateTime.now().isAfter(validUntil!);
bool get isConverted => convertedOrderId != null;
}

View File

@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quote_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class QuoteModelAdapter extends TypeAdapter<QuoteModel> {
@override
final typeId = 16;
@override
QuoteModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return QuoteModel(
quoteId: fields[0] as String,
quoteNumber: fields[1] as String,
userId: fields[2] as String,
status: fields[3] as QuoteStatus,
totalAmount: (fields[4] as num).toDouble(),
discountAmount: (fields[5] as num).toDouble(),
finalAmount: (fields[6] as num).toDouble(),
projectName: fields[7] as String?,
deliveryAddress: fields[8] as String?,
paymentTerms: fields[9] as String?,
notes: fields[10] as String?,
validUntil: fields[11] as DateTime?,
convertedOrderId: fields[12] as String?,
erpnextQuotation: fields[13] as String?,
createdAt: fields[14] as DateTime,
updatedAt: fields[15] as DateTime?,
);
}
@override
void write(BinaryWriter writer, QuoteModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.quoteId)
..writeByte(1)
..write(obj.quoteNumber)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.status)
..writeByte(4)
..write(obj.totalAmount)
..writeByte(5)
..write(obj.discountAmount)
..writeByte(6)
..write(obj.finalAmount)
..writeByte(7)
..write(obj.projectName)
..writeByte(8)
..write(obj.deliveryAddress)
..writeByte(9)
..write(obj.paymentTerms)
..writeByte(10)
..write(obj.notes)
..writeByte(11)
..write(obj.validUntil)
..writeByte(12)
..write(obj.convertedOrderId)
..writeByte(13)
..write(obj.erpnextQuotation)
..writeByte(14)
..write(obj.createdAt)
..writeByte(15)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is QuoteModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,313 @@
/// Domain Entity: Quote
///
/// Represents a price quotation for products/services.
library;
/// Quote status enum
enum QuoteStatus {
/// Quote is in draft state
draft,
/// Quote has been sent to customer
sent,
/// Customer has accepted the quote
accepted,
/// Quote has been rejected
rejected,
/// Quote has expired
expired,
/// Quote has been converted to order
converted;
/// Get display name for status
String get displayName {
switch (this) {
case QuoteStatus.draft:
return 'Draft';
case QuoteStatus.sent:
return 'Sent';
case QuoteStatus.accepted:
return 'Accepted';
case QuoteStatus.rejected:
return 'Rejected';
case QuoteStatus.expired:
return 'Expired';
case QuoteStatus.converted:
return 'Converted';
}
}
}
/// Delivery Address information
class DeliveryAddress {
/// Recipient name
final String? name;
/// Phone number
final String? phone;
/// Street address
final String? street;
/// Ward/commune
final String? ward;
/// District
final String? district;
/// City/province
final String? city;
/// Postal code
final String? postalCode;
const DeliveryAddress({
this.name,
this.phone,
this.street,
this.ward,
this.district,
this.city,
this.postalCode,
});
/// Get full address string
String get fullAddress {
final parts = [
street,
ward,
district,
city,
postalCode,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
/// Create from JSON map
factory DeliveryAddress.fromJson(Map<String, dynamic> json) {
return DeliveryAddress(
name: json['name'] as String?,
phone: json['phone'] as String?,
street: json['street'] as String?,
ward: json['ward'] as String?,
district: json['district'] as String?,
city: json['city'] as String?,
postalCode: json['postal_code'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'phone': phone,
'street': street,
'ward': ward,
'district': district,
'city': city,
'postal_code': postalCode,
};
}
}
/// Quote Entity
///
/// Contains complete quotation information:
/// - Quote identification
/// - Customer details
/// - Pricing and terms
/// - Conversion tracking
class Quote {
/// Unique quote identifier
final String quoteId;
/// Quote number (human-readable)
final String quoteNumber;
/// User ID who requested the quote
final String userId;
/// Quote status
final QuoteStatus status;
/// Total amount before discount
final double totalAmount;
/// Discount amount
final double discountAmount;
/// Final amount after discount
final double finalAmount;
/// Project name (if applicable)
final String? projectName;
/// Delivery address
final DeliveryAddress? deliveryAddress;
/// Payment terms
final String? paymentTerms;
/// Quote notes
final String? notes;
/// Valid until date
final DateTime? validUntil;
/// Converted order ID (if converted)
final String? convertedOrderId;
/// ERPNext quotation reference
final String? erpnextQuotation;
/// Quote creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
const Quote({
required this.quoteId,
required this.quoteNumber,
required this.userId,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.finalAmount,
this.projectName,
this.deliveryAddress,
this.paymentTerms,
this.notes,
this.validUntil,
this.convertedOrderId,
this.erpnextQuotation,
required this.createdAt,
required this.updatedAt,
});
/// Check if quote is draft
bool get isDraft => status == QuoteStatus.draft;
/// Check if quote is sent
bool get isSent => status == QuoteStatus.sent;
/// Check if quote is accepted
bool get isAccepted => status == QuoteStatus.accepted;
/// Check if quote is rejected
bool get isRejected => status == QuoteStatus.rejected;
/// Check if quote is expired
bool get isExpired =>
status == QuoteStatus.expired ||
(validUntil != null && DateTime.now().isAfter(validUntil!));
/// Check if quote is converted to order
bool get isConverted => status == QuoteStatus.converted;
/// Check if quote can be edited
bool get canBeEdited => isDraft;
/// Check if quote can be sent
bool get canBeSent => isDraft;
/// Check if quote can be converted to order
bool get canBeConverted => isAccepted && !isExpired;
/// Get discount percentage
double get discountPercentage {
if (totalAmount == 0) return 0;
return (discountAmount / totalAmount) * 100;
}
/// Get days until expiry
int? get daysUntilExpiry {
if (validUntil == null) return null;
final days = validUntil!.difference(DateTime.now()).inDays;
return days > 0 ? days : 0;
}
/// Check if expiring soon (within 7 days)
bool get isExpiringSoon {
if (validUntil == null || isExpired) return false;
final days = daysUntilExpiry;
return days != null && days > 0 && days <= 7;
}
/// Copy with method for immutability
Quote copyWith({
String? quoteId,
String? quoteNumber,
String? userId,
QuoteStatus? status,
double? totalAmount,
double? discountAmount,
double? finalAmount,
String? projectName,
DeliveryAddress? deliveryAddress,
String? paymentTerms,
String? notes,
DateTime? validUntil,
String? convertedOrderId,
String? erpnextQuotation,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Quote(
quoteId: quoteId ?? this.quoteId,
quoteNumber: quoteNumber ?? this.quoteNumber,
userId: userId ?? this.userId,
status: status ?? this.status,
totalAmount: totalAmount ?? this.totalAmount,
discountAmount: discountAmount ?? this.discountAmount,
finalAmount: finalAmount ?? this.finalAmount,
projectName: projectName ?? this.projectName,
deliveryAddress: deliveryAddress ?? this.deliveryAddress,
paymentTerms: paymentTerms ?? this.paymentTerms,
notes: notes ?? this.notes,
validUntil: validUntil ?? this.validUntil,
convertedOrderId: convertedOrderId ?? this.convertedOrderId,
erpnextQuotation: erpnextQuotation ?? this.erpnextQuotation,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Quote &&
other.quoteId == quoteId &&
other.quoteNumber == quoteNumber &&
other.userId == userId &&
other.status == status &&
other.totalAmount == totalAmount &&
other.discountAmount == discountAmount &&
other.finalAmount == finalAmount;
}
@override
int get hashCode {
return Object.hash(
quoteId,
quoteNumber,
userId,
status,
totalAmount,
discountAmount,
finalAmount,
);
}
@override
String toString() {
return 'Quote(quoteId: $quoteId, quoteNumber: $quoteNumber, status: $status, '
'finalAmount: $finalAmount, validUntil: $validUntil)';
}
}

View File

@@ -0,0 +1,133 @@
/// Domain Entity: Quote Item
///
/// Represents a single line item in a quotation.
library;
/// Quote Item Entity
///
/// Contains item-level information in a quote:
/// - Product reference
/// - Quantity and pricing
/// - Price negotiation
/// - Discounts
class QuoteItem {
/// Unique quote item identifier
final String quoteItemId;
/// Quote ID this item belongs to
final String quoteId;
/// Product ID
final String productId;
/// Quantity quoted
final double quantity;
/// Original/list price per unit
final double originalPrice;
/// Negotiated price per unit
final double negotiatedPrice;
/// Discount percentage
final double discountPercent;
/// Subtotal (quantity * negotiatedPrice)
final double subtotal;
/// Item notes
final String? notes;
const QuoteItem({
required this.quoteItemId,
required this.quoteId,
required this.productId,
required this.quantity,
required this.originalPrice,
required this.negotiatedPrice,
required this.discountPercent,
required this.subtotal,
this.notes,
});
/// Calculate subtotal at original price
double get subtotalAtOriginalPrice => quantity * originalPrice;
/// Calculate discount amount per unit
double get discountPerUnit => originalPrice - negotiatedPrice;
/// Calculate total discount amount
double get totalDiscountAmount =>
(quantity * originalPrice) - (quantity * negotiatedPrice);
/// Calculate effective discount percentage
double get effectiveDiscountPercentage {
if (originalPrice == 0) return 0;
return ((originalPrice - negotiatedPrice) / originalPrice) * 100;
}
/// Check if item has discount
bool get hasDiscount => negotiatedPrice < originalPrice;
/// Check if price was negotiated
bool get isNegotiated => negotiatedPrice != originalPrice;
/// Copy with method for immutability
QuoteItem copyWith({
String? quoteItemId,
String? quoteId,
String? productId,
double? quantity,
double? originalPrice,
double? negotiatedPrice,
double? discountPercent,
double? subtotal,
String? notes,
}) {
return QuoteItem(
quoteItemId: quoteItemId ?? this.quoteItemId,
quoteId: quoteId ?? this.quoteId,
productId: productId ?? this.productId,
quantity: quantity ?? this.quantity,
originalPrice: originalPrice ?? this.originalPrice,
negotiatedPrice: negotiatedPrice ?? this.negotiatedPrice,
discountPercent: discountPercent ?? this.discountPercent,
subtotal: subtotal ?? this.subtotal,
notes: notes ?? this.notes,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is QuoteItem &&
other.quoteItemId == quoteItemId &&
other.quoteId == quoteId &&
other.productId == productId &&
other.quantity == quantity &&
other.originalPrice == originalPrice &&
other.negotiatedPrice == negotiatedPrice &&
other.subtotal == subtotal;
}
@override
int get hashCode {
return Object.hash(
quoteItemId,
quoteId,
productId,
quantity,
originalPrice,
negotiatedPrice,
subtotal,
);
}
@override
String toString() {
return 'QuoteItem(quoteItemId: $quoteItemId, productId: $productId, '
'quantity: $quantity, originalPrice: $originalPrice, '
'negotiatedPrice: $negotiatedPrice, subtotal: $subtotal)';
}
}