Add AI Quick Quote widget and inline customer reassignment

- New AI Quick Quote floating button: staff type a verbal description to
  get an instant price estimate for phone/walk-in customers; detected
  color names are fuzzy-matched against inventory for stock status;
  saves draft quote under a Walk-In / Phone customer with one click
- Inline customer change on Quote Details and Job Details: always-visible
  native select with inline confirmation banner (no TomSelect dependency);
  ChangeCustomer AJAX endpoints on QuotesController and JobsController
- Quote Edit page: customer dropdown is now editable (lock removed)
- Fix AutoMapper missing CatalogCategory -> UpdateCategoryDto mapping
  that caused a crash on the catalog category Edit page
- Help docs and AI knowledge base updated for all three features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 17:02:03 -04:00
parent fc9ddc6d17
commit 8d94013895
18 changed files with 1611 additions and 37 deletions
@@ -0,0 +1,83 @@
namespace PowderCoating.Application.DTOs.AI;
/// <summary>Request from the Quick Quote widget to analyze a verbal/phone description.</summary>
public class AiQuickQuoteRequest
{
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
public int CoatCount { get; set; } = 1;
public int CompanyId { get; set; }
}
/// <summary>Result returned to the Quick Quote widget after AI analysis.</summary>
public class AiQuickQuoteResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate";
public int EstimatedMinutes { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public string Confidence { get; set; } = "Medium";
public string? Reasoning { get; set; }
/// <summary>
/// Detected color names from the description with server-resolved inventory stock status.
/// Populated by the controller after the AI call — the service populates DetectedColorName only.
/// </summary>
public List<PowderStockMatch> PowderMatches { get; set; } = new();
public decimal EstimatedUnitPrice { get; set; }
public decimal EstimatedTotal { get; set; }
public AiPricingBreakdown? Breakdown { get; set; }
}
/// <summary>Inventory stock result for a powder color the customer mentioned.</summary>
public class PowderStockMatch
{
/// <summary>Color name exactly as extracted by Claude from the customer description.</summary>
public string DetectedColorName { get; set; } = string.Empty;
/// <summary>Matched inventory item display name; null when no inventory match was found.</summary>
public string? InventoryItemName { get; set; }
public decimal QuantityOnHand { get; set; }
public decimal UnitCost { get; set; }
public bool IsInStock { get; set; }
public bool HasInventoryMatch { get; set; }
}
/// <summary>Request to persist the quick quote estimate as a draft Quote record.</summary>
public class SaveQuickQuoteRequest
{
/// <summary>Caller identifier — used as the quote CustomerPO (e.g., "John - 4 wheels").</summary>
public string Reference { get; set; } = string.Empty;
public string OriginalDescription { get; set; } = string.Empty;
public string AiDescription { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate";
public int EstimatedMinutes { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public int Quantity { get; set; } = 1;
public int CoatCount { get; set; } = 1;
public decimal EstimatedUnitPrice { get; set; }
public decimal MaterialCost { get; set; }
public decimal LaborCost { get; set; }
}
/// <summary>Internal JSON schema returned by Claude for quick quote analysis.</summary>
public class ClaudeQuickQuoteResponse
{
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate";
public int EstimatedMinutes { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public string Confidence { get; set; } = "Medium";
public string Reasoning { get; set; } = string.Empty;
/// <summary>Color/powder names verbatim from the customer description — server resolves inventory stock.</summary>
public List<string> DetectedColorNames { get; set; } = new();
}