using System.ComponentModel.DataAnnotations; using PowderCoating.Application.DTOs.PrepService; namespace PowderCoating.Application.DTOs.Quote; // ============================================================================ // LIST DTO - For Index page listing // ============================================================================ public class QuoteListDto { public int Id { get; set; } public string QuoteNumber { get; set; } = string.Empty; public string CustomerOrProspectName { get; set; } = string.Empty; // Quote Status (from lookup table) public int QuoteStatusId { get; set; } public string StatusCode { get; set; } = string.Empty; public string StatusDisplayName { get; set; } = string.Empty; public string StatusColorClass { get; set; } = "secondary"; public bool StatusIsDraft { get; set; } public DateTime QuoteDate { get; set; } public DateTime? ExpirationDate { get; set; } public decimal Total { get; set; } public string? Notes { get; set; } public string? Description { get; set; } public string? Tags { get; set; } public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft; public bool IsProspect { get; set; } } // ============================================================================ // FULL QUOTE DTO - For Details page // ============================================================================ public class QuoteDto { public int Id { get; set; } public string QuoteNumber { get; set; } = string.Empty; // Customer or Prospect Info public int? CustomerId { get; set; } public string? CustomerName { get; set; } public string? CustomerCompanyName { get; set; } public string? CustomerContactFirstName { get; set; } public string? CustomerContactLastName { get; set; } public string? CustomerEmail { get; set; } public string? CustomerPhone { get; set; } public bool CustomerNotifyByEmail { get; set; } = true; public string? CustomerMobilePhone { get; set; } public bool CustomerNotifyBySms { get; set; } public string? ProspectCompanyName { get; set; } public string? ProspectContactFirstName { get; set; } public string? ProspectContactLastName { get; set; } public string? ProspectContactName { get; set; } public string? ProspectEmail { get; set; } public string? ProspectPhone { get; set; } public string? ProspectAddress { get; set; } public string? ProspectCity { get; set; } public string? ProspectState { get; set; } public string? ProspectZipCode { get; set; } public bool ProspectSmsConsent { get; set; } public DateTime? ProspectSmsConsentedAt { get; set; } public string? PreparedById { get; set; } public string? PreparedByName { get; set; } public string? PreparedByEmail { get; set; } // Quote Status (from lookup table) public int QuoteStatusId { get; set; } public string StatusCode { get; set; } = string.Empty; public string StatusDisplayName { get; set; } = string.Empty; public string StatusColorClass { get; set; } = "secondary"; public string? StatusIconClass { get; set; } public bool StatusIsApproved { get; set; } public bool StatusIsConverted { get; set; } public bool StatusIsDraft { get; set; } public bool IsCommercial { get; set; } // Dates public DateTime QuoteDate { get; set; } public DateTime? ExpirationDate { get; set; } public DateTime? SentDate { get; set; } public DateTime? ApprovedDate { get; set; } // Pricing public decimal SubTotal { get; set; } // Discount Information public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount public decimal DiscountValue { get; set; } public decimal DiscountPercent { get; set; } public decimal DiscountAmount { get; set; } public string? DiscountReason { get; set; } public bool HideDiscountFromCustomer { get; set; } public decimal TaxPercent { get; set; } public decimal TaxAmount { get; set; } public decimal RushFee { get; set; } public decimal Total { get; set; } public bool IsRushJob { get; set; } // Additional Information public string? Description { get; set; } public string? Terms { get; set; } public string? Notes { get; set; } public string? CustomerPO { get; set; } public string? Tags { get; set; } // Items public List QuoteItems { get; set; } = new(); // Prep Services public List PrepServices { get; set; } = new(); public List PrepServiceIds { get; set; } = new(); // Pricing Breakdown public QuotePricingBreakdownDto? PricingBreakdown { get; set; } // Oven selection public int? OvenCostId { get; set; } public string? OvenLabel { get; set; } // Oven batch pricing public int OvenBatches { get; set; } = 1; public int? OvenCycleMinutes { get; set; } // Conversion Tracking public int? ConvertedToJobId { get; set; } public string? ConvertedToJobNumber { get; set; } // Customer Approval Tracking public string? ApprovalToken { get; set; } public DateTime? ApprovalTokenExpiresAt { get; set; } public DateTime? ApprovalTokenUsedAt { get; set; } public string? DeclineReason { get; set; } public bool IsProspect => !CustomerId.HasValue; public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft; public string CustomerOrProspectName => CustomerName ?? ProspectCompanyName ?? ProspectContactName ?? "Unknown"; } // ============================================================================ // CREATE QUOTE DTO - For creating new quotes // ============================================================================ public class CreateQuoteDto { // Customer or Prospect Selection public bool IsForProspect { get; set; } [Display(Name = "Customer")] public int? CustomerId { get; set; } // Prospect Contact Information [Display(Name = "Company Name")] [StringLength(100)] public string? ProspectCompanyName { get; set; } [Display(Name = "Contact Name")] [StringLength(100)] public string? ProspectContactName { get; set; } [Display(Name = "Email")] [EmailAddress] [StringLength(100)] public string? ProspectEmail { get; set; } [Display(Name = "Phone")] [Phone] [StringLength(20)] public string? ProspectPhone { get; set; } [Display(Name = "Address")] [StringLength(200)] public string? ProspectAddress { get; set; } [Display(Name = "City")] [StringLength(100)] public string? ProspectCity { get; set; } [Display(Name = "State")] [StringLength(2)] public string? ProspectState { get; set; } [Display(Name = "Zip Code")] [StringLength(10)] public string? ProspectZipCode { get; set; } [Display(Name = "SMS Consent")] public bool ProspectSmsConsent { get; set; } = false; // Oven Selection [Display(Name = "Oven")] public int? OvenCostId { get; set; } // Oven Batch Pricing [Display(Name = "Oven Batches")] [Range(1, 9999)] public int OvenBatches { get; set; } = 1; [Display(Name = "Oven Cycle Time (min)")] [Range(1, 1440)] public int? OvenCycleMinutes { get; set; } // Quote Information [Display(Name = "Commercial Quote")] public bool IsCommercial { get; set; } [Display(Name = "Rush Job")] public bool IsRushJob { get; set; } [Display(Name = "Quote Date")] [DataType(DataType.Date)] public DateTime QuoteDate { get; set; } = DateTime.Today; [Display(Name = "Expiration Date")] [DataType(DataType.Date)] public DateTime? ExpirationDate { get; set; } [Display(Name = "Description")] [StringLength(500)] public string? Description { get; set; } [Display(Name = "Terms & Conditions")] [DataType(DataType.MultilineText)] public string? Terms { get; set; } [Display(Name = "Internal Notes")] [DataType(DataType.MultilineText)] public string? Notes { get; set; } [Display(Name = "Customer PO Number")] [StringLength(50)] public string? CustomerPO { get; set; } [Display(Name = "Tags")] [StringLength(500)] public string? Tags { get; set; } // Pricing [Display(Name = "Tax Percent (%)")] [Range(0, 100)] public decimal TaxPercent { get; set; } // Discount [Display(Name = "Discount Type")] public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount [Display(Name = "Discount Value")] [Range(0, 999999)] public decimal DiscountValue { get; set; } [Display(Name = "Discount Reason")] [StringLength(200)] public string? DiscountReason { get; set; } [Display(Name = "Hide discount from customer")] public bool HideDiscountFromCustomer { get; set; } = false; // Items [Required] // Note: MinLength validation removed to prevent false positives on initial page load // JavaScript validation handles this check on form submission public List QuoteItems { get; set; } = new(); // Prep Services [Display(Name = "Preparation Services")] public List PrepServiceIds { get; set; } = new(); // Email Options [Display(Name = "Send quote via email")] public bool SendEmailToCustomer { get; set; } = false; // AI Photo TempIds — list of uploaded temp photo IDs to promote on save public List AiPhotoTempIds { get; set; } = new(); // Quote Photo TempIds — general (non-AI) photos staged on the Create page public List QuotePhotoTempIds { get; set; } = new(); public List QuotePhotoFileNames { get; set; } = new(); } // ============================================================================ // UPDATE QUOTE DTO - For editing existing quotes // ============================================================================ public class UpdateQuoteDto { public int Id { get; set; } // Customer or Prospect Info (read-only after creation, but included for completeness) public int? CustomerId { get; set; } public bool IsForProspect { get; set; } // Prospect Contact Information [Display(Name = "Company Name")] [StringLength(100)] public string? ProspectCompanyName { get; set; } [Display(Name = "Contact Name")] [StringLength(100)] public string? ProspectContactName { get; set; } [Display(Name = "Email")] [EmailAddress] [StringLength(100)] public string? ProspectEmail { get; set; } [Display(Name = "Phone")] [Phone] [StringLength(20)] public string? ProspectPhone { get; set; } [Display(Name = "Address")] [StringLength(200)] public string? ProspectAddress { get; set; } [Display(Name = "City")] [StringLength(100)] public string? ProspectCity { get; set; } [Display(Name = "State")] [StringLength(2)] public string? ProspectState { get; set; } [Display(Name = "Zip Code")] [StringLength(10)] public string? ProspectZipCode { get; set; } [Display(Name = "SMS Consent")] public bool ProspectSmsConsent { get; set; } = false; // Oven Selection [Display(Name = "Oven")] public int? OvenCostId { get; set; } // Oven Batch Pricing [Display(Name = "Oven Batches")] [Range(1, 9999)] public int OvenBatches { get; set; } = 1; [Display(Name = "Oven Cycle Time (min)")] [Range(1, 1440)] public int? OvenCycleMinutes { get; set; } // Quote Information [Display(Name = "Status")] public int QuoteStatusId { get; set; } // FK to lookup table [Display(Name = "Commercial Quote")] public bool IsCommercial { get; set; } [Display(Name = "Rush Job")] public bool IsRushJob { get; set; } [Display(Name = "Quote Date")] [DataType(DataType.Date)] public DateTime QuoteDate { get; set; } [Display(Name = "Expiration Date")] [DataType(DataType.Date)] public DateTime? ExpirationDate { get; set; } [Display(Name = "Description")] [StringLength(500)] public string? Description { get; set; } [Display(Name = "Terms & Conditions")] [DataType(DataType.MultilineText)] public string? Terms { get; set; } [Display(Name = "Internal Notes")] [DataType(DataType.MultilineText)] public string? Notes { get; set; } [Display(Name = "Customer PO Number")] [StringLength(50)] public string? CustomerPO { get; set; } [Display(Name = "Tags")] [StringLength(500)] public string? Tags { get; set; } // Pricing [Display(Name = "Tax Percent (%)")] [Range(0, 100)] public decimal TaxPercent { get; set; } // Discount [Display(Name = "Discount Type")] public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount [Display(Name = "Discount Value")] [Range(0, 999999)] public decimal DiscountValue { get; set; } [Display(Name = "Discount Reason")] [StringLength(200)] public string? DiscountReason { get; set; } [Display(Name = "Hide discount from customer")] public bool HideDiscountFromCustomer { get; set; } = false; // Items [Required] // Note: MinLength validation removed to prevent false positives on initial page load // JavaScript validation handles this check on form submission public List QuoteItems { get; set; } = new(); // Prep Services [Display(Name = "Preparation Services")] public List PrepServiceIds { get; set; } = new(); // AI Photo TempIds — list of uploaded temp photo IDs to promote on save public List AiPhotoTempIds { get; set; } = new(); // Quote Photo TempIds — general (non-AI) photos staged on the Edit page (not used by direct-upload Edit path, // kept for symmetry / future use) public List QuotePhotoTempIds { get; set; } = new(); public List QuotePhotoFileNames { get; set; } = new(); } // ============================================================================ // QUOTE ITEM DTO - For displaying quote items // ============================================================================ public class QuoteItemDto { public int Id { get; set; } public int QuoteId { get; set; } [Display(Name = "Description")] public string Description { get; set; } = string.Empty; [Display(Name = "Quantity")] public decimal Quantity { get; set; } [Display(Name = "Surface Area (sq ft)")] public decimal SurfaceAreaSqFt { get; set; } [Display(Name = "Estimated Minutes")] public int EstimatedMinutes { get; set; } [Display(Name = "Requires Sandblasting")] public bool RequiresSandblasting { get; set; } [Display(Name = "Requires Masking")] public bool RequiresMasking { get; set; } [Display(Name = "Catalog Item")] public int? CatalogItemId { get; set; } public string? CatalogItemName { get; set; } [Display(Name = "Unit Price")] public decimal UnitPrice { get; set; } [Display(Name = "Total Price")] public decimal TotalPrice { get; set; } public string? Notes { get; set; } public bool IsGenericItem { get; set; } public bool IsLaborItem { get; set; } public bool IsSalesItem { get; set; } public string? Sku { get; set; } public decimal? ManualUnitPrice { get; set; } // Coating layers public List Coats { get; set; } = new(); // Preparation services public List PrepServices { get; set; } = new(); // Part complexity level for calculated items public string? Complexity { get; set; } public bool IsAiItem { get; set; } // Cost breakdown snapshot public decimal ItemMaterialCost { get; set; } public decimal ItemLaborCost { get; set; } public decimal ItemEquipmentCost { get; set; } } // ============================================================================ // CREATE QUOTE ITEM DTO - For creating quote items // ============================================================================ public class CreateQuoteItemDto { // Description is required for calculated items but auto-populated for catalog items [Display(Name = "Description")] [StringLength(200)] public string Description { get; set; } = string.Empty; [Required] [Display(Name = "Quantity")] [Range(0.01, 10000)] public decimal Quantity { get; set; } = 1; [Required] [Display(Name = "Surface Area (sq ft)")] [Range(0, 100000)] // 0 is valid for generic items public decimal SurfaceAreaSqFt { get; set; } [Required] [Display(Name = "Estimated Minutes")] [Range(0, 10000, ErrorMessage = "Estimated minutes must be between 0 and 10,000 (0 for catalog items)")] public int EstimatedMinutes { get; set; } [Display(Name = "Requires Sandblasting")] public bool RequiresSandblasting { get; set; } [Display(Name = "Requires Masking")] public bool RequiresMasking { get; set; } [Display(Name = "Catalog Item")] public int? CatalogItemId { get; set; } [Display(Name = "Notes")] [StringLength(500)] public string? Notes { get; set; } // Generic item — price entered manually, bypasses calculation engine public bool IsGenericItem { get; set; } [Range(0, 1000000)] public decimal? ManualUnitPrice { get; set; } // Labor item — Quantity = hours; priced using StandardLaborRate × markup, no coats public bool IsLaborItem { get; set; } // Sales item — off-the-shelf merchandise (T-shirts, tumblers, etc.) public bool IsSalesItem { get; set; } public string? Sku { get; set; } // Coating layers public List Coats { get; set; } = new(); // Prep services (each has its own time estimate) // For catalog items, prep cost is only included when IncludePrepCost = true public List PrepServices { get; set; } = new(); // Controls whether prep service labor cost is added to the item price. // Always true for calculated items; catalog items default to false (costs baked in) // but can be opted in via the wizard toggle. public bool IncludePrepCost { get; set; } = true; // Part complexity — drives a price multiplier on calculated items only // Values: null | "Simple" | "Moderate" | "Complex" | "Extreme" public string? Complexity { get; set; } // Optional unit price override for catalog items — overrides the catalog default price public decimal? PowderCostOverride { get; set; } // True when this item was generated via AI photo analysis public bool IsAiItem { get; set; } // AI-generated standardized tags (comma-separated, e.g. "automotive,tubular") public string? AiTags { get; set; } // ID of the AiItemPrediction record captured at analysis time (null for non-AI items) public int? AiPredictionId { get; set; } } // ============================================================================ // QUOTE ITEM PREP SERVICE DTO // ============================================================================ public class CreateQuoteItemPrepServiceDto { [Required] public int PrepServiceId { get; set; } [Required] [Range(0, 10000)] public int EstimatedMinutes { get; set; } /// Blast setup selected in wizard for this sandblasting prep service. public int? BlastSetupId { get; set; } // Populated when mapping from entity (for display purposes) public string? PrepServiceName { get; set; } } // ============================================================================ // PRICING BREAKDOWN DTO - For transparent cost breakdown // ============================================================================ public class QuotePricingBreakdownDto { // Per-Item Costs public decimal MaterialCosts { get; set; } public decimal LaborCosts { get; set; } public decimal EquipmentCosts { get; set; } public decimal ItemSubtotal { get; set; } // Quote-Level Costs public decimal ItemsSubtotal { get; set; } public decimal ShopSuppliesAmount { get; set; } public decimal ShopSuppliesPercent { get; set; } public decimal OverheadCosts { get; set; } public decimal OverheadPercent { get; set; } public decimal ProfitMargin { get; set; } public decimal ProfitPercent { get; set; } public decimal SubtotalBeforeDiscount { get; set; } public decimal PricingTierDiscountAmount { get; set; } public decimal PricingTierDiscountPercent { get; set; } public decimal QuoteDiscountAmount { get; set; } public decimal QuoteDiscountPercent { get; set; } public decimal DiscountAmount { get; set; } public decimal DiscountPercent { get; set; } public decimal RushFee { get; set; } public decimal SubtotalAfterDiscount { get; set; } public decimal TaxAmount { get; set; } public decimal TaxPercent { get; set; } public decimal OvenBatchCost { get; set; } public int OvenBatches { get; set; } public int OvenCycleMinutes { get; set; } public decimal FacilityOverheadCost { get; set; } public decimal FacilityOverheadRatePerHour { get; set; } public decimal Total { get; set; } // Cost Breakdown Details public string CostBreakdownDetails { get; set; } = string.Empty; } // ============================================================================ // CONVERT TO CUSTOMER DTO - For prospect-to-customer conversion // ============================================================================ public class ConvertQuoteToCustomerDto { public int QuoteId { get; set; } public string QuoteNumber { get; set; } = string.Empty; // Pre-filled from prospect data [Display(Name = "Company Name")] [StringLength(100)] public string? CompanyName { get; set; } [Required] [Display(Name = "Contact Name")] [StringLength(100)] public string ContactName { get; set; } = string.Empty; [Display(Name = "Email")] [EmailAddress] [StringLength(100)] public string? Email { get; set; } [Display(Name = "Phone")] [Phone] [StringLength(20)] public string? Phone { get; set; } [Display(Name = "Address")] [StringLength(200)] public string? Address { get; set; } [Display(Name = "City")] [StringLength(100)] public string? City { get; set; } [Display(Name = "State")] [StringLength(2)] public string? State { get; set; } [Display(Name = "Zip Code")] [StringLength(10)] public string? ZipCode { get; set; } // Additional customer fields [Display(Name = "Tax ID / EIN")] [StringLength(50)] public string? TaxId { get; set; } [Display(Name = "Credit Limit")] [Range(0, 1000000)] public decimal CreditLimit { get; set; } [Display(Name = "Pricing Tier")] public int? PricingTierId { get; set; } [Display(Name = "Customer Type")] public bool IsCommercial { get; set; } = true; [Display(Name = "Payment Terms")] [StringLength(100)] public string? PaymentTerms { get; set; } [Display(Name = "Notes")] [DataType(DataType.MultilineText)] public string? Notes { get; set; } /// /// Staff must explicitly confirm verbal SMS consent before it carries over to the new customer record. /// Pre-checked when ProspectSmsConsent was true on the source quote. /// [Display(Name = "Customer has given verbal consent to receive SMS notifications")] public bool SmsConsent { get; set; } /// Timestamp from the source quote — preserved so the consent record reflects when consent was originally given. public DateTime? ProspectSmsConsentedAt { get; set; } } // ============================================================================ // QUOTE ITEM COAT DTOs - For multi-coat support // ============================================================================ /// /// DTO for creating/editing a coating layer on a quote item /// public class CreateQuoteItemCoatDto { [Required] [StringLength(100)] [Display(Name = "Coat Name")] public string CoatName { get; set; } = "Base Coat"; public int Sequence { get; set; } = 1; [Display(Name = "Inventory Powder")] public int? InventoryItemId { get; set; } [StringLength(100)] [Display(Name = "Color Name")] public string? ColorName { get; set; } [Display(Name = "Vendor")] public int? VendorId { get; set; } [StringLength(50)] [Display(Name = "Color Code")] public string? ColorCode { get; set; } [StringLength(50)] [Display(Name = "Finish")] public string? Finish { get; set; } [Range(1, 500)] [Display(Name = "Coverage (sq ft/lb)")] public decimal CoverageSqFtPerLb { get; set; } = 30m; [Range(1, 100)] [Display(Name = "Transfer Efficiency (%)")] public decimal TransferEfficiency { get; set; } = 65m; [Range(0, 1000)] [Display(Name = "Powder Cost ($/lb)")] public decimal? PowderCostPerLb { get; set; } [Range(0, 10000)] [Display(Name = "Powder to Order (lbs)")] public decimal? PowderToOrder { get; set; } [StringLength(500)] [Display(Name = "Notes")] public string? Notes { get; set; } /// /// When true, the additional layer labor charge is not applied even if this is not the first coat. /// public bool NoExtraLayerCharge { get; set; } /// Platform powder catalog item ID selected via the Custom tab lookup. public int? CatalogItemId { get; set; } /// /// When true (and CatalogItemId is set), the server creates a 0-balance IsIncoming inventory /// item from the catalog entry so QR codes can be printed while the powder is in transit. /// The coat is then linked to that new inventory record. /// public bool AddAsIncoming { get; set; } } /// /// DTO for displaying a coating layer on a quote item /// public class QuoteItemCoatDto { public int Id { get; set; } public string CoatName { get; set; } = string.Empty; public int Sequence { get; set; } public int? InventoryItemId { get; set; } public string? InventoryItemName { get; set; } public int? VendorId { get; set; } public string? VendorName { get; set; } public string? ColorName { get; set; } public string? ColorCode { get; set; } public string? Finish { get; set; } public decimal CoverageSqFtPerLb { get; set; } public decimal TransferEfficiency { get; set; } public decimal? PowderToOrder { get; set; } public decimal CoatMaterialCost { get; set; } public decimal CoatLaborCost { get; set; } public decimal CoatTotalCost { get; set; } public string? Notes { get; set; } } // ============================================================================ // PRICING CALCULATION RESULT - Internal use for pricing service // ============================================================================ /// /// Result of calculating costs for a single coating layer /// public class QuoteItemCoatPricingResult { public decimal CoatMaterialCost { get; set; } public decimal CoatLaborCost { get; set; } public decimal CoatTotalCost { get; set; } } public class QuoteItemPricingResult { public decimal MaterialCost { get; set; } public decimal LaborCost { get; set; } public decimal EquipmentCost { get; set; } public decimal ItemSubtotal { get; set; } public decimal UnitPrice { get; set; } public decimal TotalPrice { get; set; } } public class QuotePricingResult { public decimal ItemsSubtotal { get; set; } public decimal ShopSuppliesAmount { get; set; } public decimal ShopSuppliesPercent { get; set; } public decimal OverheadCosts { get; set; } public decimal OverheadPercent { get; set; } public decimal ProfitMargin { get; set; } public decimal ProfitPercent { get; set; } public decimal SubtotalBeforeDiscount { get; set; } // Pricing Tier Discount (from customer's pricing tier) public decimal PricingTierDiscountAmount { get; set; } public decimal PricingTierDiscountPercent { get; set; } // Quote-Level Discount (applied on the quote) public decimal QuoteDiscountAmount { get; set; } public decimal QuoteDiscountPercent { get; set; } // Combined Discount (for backward compatibility) public decimal DiscountAmount { get; set; } public decimal DiscountPercent { get; set; } public decimal SubtotalAfterDiscount { get; set; } public decimal RushFee { get; set; } public decimal TaxAmount { get; set; } public decimal TaxPercent { get; set; } public decimal Total { get; set; } // Oven batch cost (quote-level, separate from per-item calculations) public decimal OvenBatchCost { get; set; } public int OvenBatches { get; set; } public int OvenCycleMinutes { get; set; } // Facility overhead (rent + utilities apportioned by estimated job hours) public decimal FacilityOverheadCost { get; set; } public decimal FacilityOverheadRatePerHour { get; set; } // Detailed breakdown for transparency public decimal MaterialCosts { get; set; } public decimal LaborCosts { get; set; } public decimal EquipmentCosts { get; set; } // Per-item results (same order as input items) public List ItemResults { get; set; } = new(); }