From 3327c869091cc8c9bd835c7a50cb3ce8d9c1b4e6 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Fri, 24 Apr 2026 21:29:42 -0400 Subject: [PATCH] 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 --- .../Controllers/CompanySettingsController.cs | 141 ++++++++++++++++++ .../Views/CompanySettings/Index.cshtml | 47 +++++- .../Views/SetupWizard/Step3.cshtml | 5 +- .../Views/Shared/_Layout.cshtml | 2 +- 4 files changed, 187 insertions(+), 8 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/CompanySettingsController.cs b/src/PowderCoating.Web/Controllers/CompanySettingsController.cs index 0c6391e..546c5e2 100644 --- a/src/PowderCoating.Web/Controllers/CompanySettingsController.cs +++ b/src/PowderCoating.Web/Controllers/CompanySettingsController.cs @@ -620,6 +620,147 @@ public class CompanySettingsController : Controller } } + /// + /// 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. + /// + // GET: CompanySettings/GenerateAiProfileDraft + [HttpGet] + public async Task 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(); + 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(); + 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." }); + } + } + /// /// Saves the Quoting Calibration / Shop Capability profile. Maps equipment fields onto /// and returns the freshly derived blast and coating diff --git a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml index f128352..81b07d5 100644 --- a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml +++ b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml @@ -370,7 +370,7 @@ -
+
@@ -395,7 +395,10 @@
- +
+ + hrs +
Typical: 160 hrs (4 wks × 40 hrs)
@@ -757,10 +760,16 @@ @(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)/2000
- - +
+ + + +
@@ -2274,6 +2283,32 @@ }); }); + $('#btnGenerateAiDraft').on('click', function () { + const btn = $(this); + const existing = $('#aiContextProfile').val().trim(); + if (existing && !confirm('This will replace your current profile text with a generated draft. Continue?')) return; + btn.prop('disabled', true).html(' Generating...'); + $.ajax({ + url: '@Url.Action("GenerateAiProfileDraft", "CompanySettings")', + type: 'GET', + success: function (response) { + if (response.success) { + $('#aiContextProfile').val(response.draft); + $('#aiProfileCharCount').text(response.draft.length); + showToast('info', 'Draft generated — review and edit it, then click Save AI Profile.'); + } else { + showToast('error', response.message); + } + }, + error: function () { + showToast('error', 'An error occurred while generating the draft.'); + }, + complete: function () { + btn.prop('disabled', false).html(' Generate from my settings'); + } + }); + }); + // Quoting Calibration — save $('#saveBlastProfile').on('click', function () { var btn = $(this); diff --git a/src/PowderCoating.Web/Views/SetupWizard/Step3.cshtml b/src/PowderCoating.Web/Views/SetupWizard/Step3.cshtml index 8c5f467..85a97b9 100644 --- a/src/PowderCoating.Web/Views/SetupWizard/Step3.cshtml +++ b/src/PowderCoating.Web/Views/SetupWizard/Step3.cshtml @@ -88,7 +88,10 @@
- +
+ + hrs +
Hours per month the shop is actively producing work. Default: 160 (4 wks × 40 hrs).
diff --git a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml index 7653463..e145e03 100644 --- a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml +++ b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml @@ -2089,7 +2089,7 @@ @if (User.Identity?.IsAuthenticated == true) { - @await Html.PartialAsync("_AiQuickQuoteWidget") + @* @await Html.PartialAsync("_AiQuickQuoteWidget") *@ @await Html.PartialAsync("_AiHelpWidget") }