Add AI Profile draft generator and hide AI Quick Quote for release

- GenerateAiProfileDraft endpoint builds suggested AI Profile text from
  existing company config (ovens, workers, inventory categories, rates)
- "Generate from my settings" button wired in Company Settings AI Profile tab
- Add "hrs" unit label to Billable Hours/Month input in Company Settings and Setup Wizard Step 3
- Hide AI Quick Quote widget (commented out in _Layout) pending next release

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:29:42 -04:00
parent 27ac793f62
commit 3327c86909
4 changed files with 187 additions and 8 deletions
@@ -620,6 +620,147 @@ public class CompanySettingsController : Controller
}
}
/// <summary>
/// Builds a suggested AI profile draft from existing company configuration — company name/location,
/// named ovens, sandblasting capability, shop worker roles, coating inventory categories, and
/// operating cost rates. Returns a pre-filled paragraph the user can review and edit before saving.
/// </summary>
// GET: CompanySettings/GenerateAiProfileDraft
[HttpGet]
public async Task<IActionResult> GenerateAiProfileDraft()
{
try
{
var companyId = _tenantContext.GetCurrentCompanyId();
if (companyId == null)
return Json(new { success = false, message = "No company found." });
var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value, false, c => c.OperatingCosts);
if (company == null)
return Json(new { success = false, message = "Company not found." });
var costs = company.OperatingCosts;
var ovens = (await _unitOfWork.OvenCosts.FindAsync(o => o.IsActive)).OrderBy(o => o.DisplayOrder).ToList();
var workers = (await _unitOfWork.ShopWorkers.FindAsync(w => w.IsActive)).ToList();
var coatingCategories = (await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.IsCoating)).ToList();
var sb = new System.Text.StringBuilder();
// Opening line
var location = new[] { company.City, company.State }.Where(s => !string.IsNullOrWhiteSpace(s));
var locationStr = string.Join(", ", location);
sb.Append(company.CompanyName);
if (!string.IsNullOrWhiteSpace(locationStr))
sb.Append($" is a powder coating shop based in {locationStr}.");
else
sb.Append(" is a powder coating shop.");
sb.AppendLine();
sb.AppendLine();
// Shop size
if (costs != null)
{
var tierLabel = costs.ShopCapabilityTier switch
{
ShopCapabilityTier.Garage => "garage/hobbyist",
ShopCapabilityTier.Small => "small",
ShopCapabilityTier.Medium => "medium-sized",
ShopCapabilityTier.Large => "high-volume",
_ => "small"
};
sb.AppendLine($"We are a {tierLabel} operation" +
(workers.Count > 0 ? $" with {workers.Count} active shop worker{(workers.Count == 1 ? "" : "s")}." : "."));
}
// Ovens
if (ovens.Any())
{
sb.AppendLine();
sb.AppendLine("Our curing ovens:");
foreach (var oven in ovens)
{
var parts = new List<string>();
if (oven.MaxLoadSqFt.HasValue && oven.MaxLoadSqFt > 0)
parts.Add($"{oven.MaxLoadSqFt:0} sq ft capacity");
if (oven.DefaultCycleMinutes.HasValue && oven.DefaultCycleMinutes > 0)
parts.Add($"{oven.DefaultCycleMinutes} min cure cycle");
var detail = parts.Any() ? $" ({string.Join(", ", parts)})" : "";
sb.AppendLine($"• {oven.Label}{detail}");
}
}
// Equipment capabilities inferred from rates
if (costs != null)
{
var capabilities = new List<string>();
if (costs.SandblasterCostPerHour > 0)
capabilities.Add("sandblasting / media blasting");
if (costs.CoatingBoothCostPerHour > 0)
capabilities.Add("powder coating booth");
if (capabilities.Any())
{
sb.AppendLine();
sb.AppendLine($"We have in-house {string.Join(" and ", capabilities)} capability.");
}
}
// Powder/coating categories
if (coatingCategories.Any())
{
sb.AppendLine();
var catNames = coatingCategories.Select(c => c.DisplayName).ToList();
sb.AppendLine($"Powder categories we stock: {string.Join(", ", catNames)}.");
}
// Worker roles
if (workers.Any())
{
var roles = workers
.Select(w => w.Role)
.Distinct()
.Select(r => r switch
{
ShopWorkerRole.Sandblaster => "sandblasting",
ShopWorkerRole.Coater => "powder coating",
ShopWorkerRole.Masker => "masking",
ShopWorkerRole.QualityControl => "quality control",
ShopWorkerRole.OvenOperator => "oven operation",
ShopWorkerRole.Supervisor => "supervision",
ShopWorkerRole.Maintenance => "equipment maintenance",
_ => "general labor"
})
.Distinct()
.ToList();
if (roles.Count > 1)
{
sb.AppendLine();
sb.AppendLine($"Staff specialties on hand: {string.Join(", ", roles)}.");
}
}
// Rates hint
if (costs != null && costs.StandardLaborRate > 0)
{
sb.AppendLine();
sb.AppendLine($"Our standard labor rate is ${costs.StandardLaborRate:0.00}/hr. " +
$"We target approximately {costs.GeneralMarkupPercentage:0}% markup on all jobs.");
}
sb.AppendLine();
sb.AppendLine("(Edit this profile to add detail about the types of parts you typically coat, " +
"any brands of powder you prefer, your cure temperature, or anything else that " +
"helps the AI understand your shop better.)");
return Json(new { success = true, draft = sb.ToString().Trim() });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating AI profile draft");
return Json(new { success = false, message = "An error occurred while generating the draft." });
}
}
/// <summary>
/// Saves the Quoting Calibration / Shop Capability profile. Maps equipment fields onto
/// <see cref="CompanyOperatingCosts"/> and returns the freshly derived blast and coating