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