Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Quote/QuoteDtos.cs
T
spouliot a7ad0e1de8 Add Custom Powder Order line item and fix CSV import FinalPrice crash
Custom powder/incoming powder material cost now flows into a separate
auto-generated 'Custom Powder Order' line item instead of rolling into
individual item prices, so users can add shipping charges before the
customer sees the total. A dashed yellow preview card in the wizard
shows the material cost and lets users edit the total (including shipping)
before saving. After first save the price is user-owned.

Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric
value (e.g. 'false' from a spreadsheet formula): the job CSV importer now
streams rows one at a time with a lenient decimal converter, treating bad
values as $0 with a per-row warning instead of aborting the entire import.

Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with
Custom Powder Order behavior and a new Data Import / Export section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:37:46 -04:00

893 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<QuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
public List<PrepServiceDto> PrepServices { get; set; } = new();
public List<int> 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<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> 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<string> AiPhotoTempIds { get; set; } = new();
// Quote Photo TempIds — general (non-AI) photos staged on the Create page
public List<string> QuotePhotoTempIds { get; set; } = new();
public List<string> 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<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> PrepServiceIds { get; set; } = new();
// AI Photo TempIds — list of uploaded temp photo IDs to promote on save
public List<string> 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<string> QuotePhotoTempIds { get; set; } = new();
public List<string> 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<QuoteItemCoatDto> Coats { get; set; } = new();
// Preparation services
public List<CreateQuoteItemPrepServiceDto> PrepServices { get; set; } = new();
// Part complexity level for calculated items
public string? Complexity { get; set; }
public bool IsAiItem { get; set; }
// Custom formula item
public bool IsCustomFormulaItem { get; set; }
public int? CustomItemTemplateId { get; set; }
public string? FormulaFieldValuesJson { 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<CreateQuoteItemCoatDto> 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<CreateQuoteItemPrepServiceDto> 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; }
// Custom formula item routing — see IsCustomFormulaItem in PricingCalculationService
public bool IsCustomFormulaItem { get; set; }
public int? CustomItemTemplateId { get; set; }
public string? FormulaFieldValuesJson { 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; }
/// <summary>Blast setup selected in wizard for this sandblasting prep service.</summary>
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; }
/// <summary>
/// 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.
/// </summary>
[Display(Name = "Customer has given verbal consent to receive SMS notifications")]
public bool SmsConsent { get; set; }
/// <summary>Timestamp from the source quote — preserved so the consent record reflects when consent was originally given.</summary>
public DateTime? ProspectSmsConsentedAt { get; set; }
}
// ============================================================================
// QUOTE ITEM COAT DTOs - For multi-coat support
// ============================================================================
/// <summary>
/// DTO for creating/editing a coating layer on a quote item
/// </summary>
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; }
/// <summary>
/// When true, the additional layer labor charge is not applied even if this is not the first coat.
/// </summary>
public bool NoExtraLayerCharge { get; set; }
/// <summary>Platform powder catalog item ID selected via the Custom tab lookup.</summary>
public int? CatalogItemId { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool AddAsIncoming { get; set; }
}
/// <summary>
/// DTO for displaying a coating layer on a quote item
/// </summary>
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 bool NoExtraLayerCharge { get; set; }
public string? Notes { get; set; }
}
// ============================================================================
// PRICING CALCULATION RESULT - Internal use for pricing service
// ============================================================================
/// <summary>
/// Result of calculating costs for a single coating layer
/// </summary>
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<QuoteItemPricingResult> ItemResults { get; set; } = new();
// Pending Custom Powder Order preview — populated only when no "Custom Powder Order" item
// exists yet (first save scenario). Amount and color list let the UI show a preview row.
public decimal CustomPowderOrderAmount { get; set; }
public List<string> CustomPowderOrderColors { get; set; } = new();
}