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:
@@ -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>
|
/// <summary>
|
||||||
/// Saves the Quoting Calibration / Shop Capability profile. Maps equipment fields onto
|
/// Saves the Quoting Calibration / Shop Capability profile. Maps equipment fields onto
|
||||||
/// <see cref="CompanyOperatingCosts"/> and returns the freshly derived blast and coating
|
/// <see cref="CompanyOperatingCosts"/> and returns the freshly derived blast and coating
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
<div class="row align-items-end">
|
<div class="row align-items-start">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="monthlyRent" class="form-label">Monthly Rent</label>
|
<label for="monthlyRent" class="form-label">Monthly Rent</label>
|
||||||
@@ -395,7 +395,10 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="monthlyBillableHours" class="form-label">Billable Hours/Month</label>
|
<label for="monthlyBillableHours" class="form-label">Billable Hours/Month</label>
|
||||||
<input type="number" step="1" class="form-control facility-overhead-input" id="monthlyBillableHours" name="MonthlyBillableHours" value="@(Model.OperatingCosts?.MonthlyBillableHours ?? 160)" min="1" max="10000">
|
<div class="input-group">
|
||||||
|
<input type="number" step="1" class="form-control facility-overhead-input" id="monthlyBillableHours" name="MonthlyBillableHours" value="@(Model.OperatingCosts?.MonthlyBillableHours ?? 160)" min="1" max="10000">
|
||||||
|
<span class="input-group-text">hrs</span>
|
||||||
|
</div>
|
||||||
<small class="text-muted">Typical: 160 hrs (4 wks × 40 hrs)</small>
|
<small class="text-muted">Typical: 160 hrs (4 wks × 40 hrs)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -757,10 +760,16 @@
|
|||||||
<small class="text-muted"><span id="aiProfileCharCount">@(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)</span>/2000</small>
|
<small class="text-muted"><span id="aiProfileCharCount">@(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)</span>/2000</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary" id="btnSaveAiProfile">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<i class="bi bi-floppy me-1"></i> Save AI Profile
|
<button type="button" class="btn btn-primary" id="btnSaveAiProfile">
|
||||||
</button>
|
<i class="bi bi-floppy me-1"></i> Save AI Profile
|
||||||
<span id="aiProfileStatus" class="ms-3 small"></span>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="btnGenerateAiDraft"
|
||||||
|
title="Build a suggested profile from your existing settings — ovens, workers, inventory categories, and rates">
|
||||||
|
<i class="bi bi-stars me-1"></i> Generate from my settings
|
||||||
|
</button>
|
||||||
|
<span id="aiProfileStatus" class="small"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
@@ -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('<span class="spinner-border spinner-border-sm"></span> 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('<i class="bi bi-stars me-1"></i> Generate from my settings');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Quoting Calibration — save
|
// Quoting Calibration — save
|
||||||
$('#saveBlastProfile').on('click', function () {
|
$('#saveBlastProfile').on('click', function () {
|
||||||
var btn = $(this);
|
var btn = $(this);
|
||||||
|
|||||||
@@ -88,7 +88,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label asp-for="MonthlyBillableHours" class="form-label fw-semibold"></label>
|
<label asp-for="MonthlyBillableHours" class="form-label fw-semibold"></label>
|
||||||
<input asp-for="MonthlyBillableHours" class="form-control wz-overhead" step="1" type="number" min="1" />
|
<div class="input-group">
|
||||||
|
<input asp-for="MonthlyBillableHours" class="form-control wz-overhead" step="1" type="number" min="1" />
|
||||||
|
<span class="input-group-text">hrs</span>
|
||||||
|
</div>
|
||||||
<div class="form-text">Hours per month the shop is actively producing work. Default: 160 (4 wks × 40 hrs).</div>
|
<div class="form-text">Hours per month the shop is actively producing work. Default: 160 (4 wks × 40 hrs).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
|||||||
@@ -2089,7 +2089,7 @@
|
|||||||
|
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
@await Html.PartialAsync("_AiQuickQuoteWidget")
|
@* @await Html.PartialAsync("_AiQuickQuoteWidget") *@
|
||||||
@await Html.PartialAsync("_AiHelpWidget")
|
@await Html.PartialAsync("_AiHelpWidget")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user