314 lines
7.2 KiB
Dart
314 lines
7.2 KiB
Dart
/// 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)';
|
|
}
|
|
}
|