Files
PowderCoatingLogix/src/PowderCoating.Web/Views/PowderInsights/Index.cshtml
T
spouliot 64a9c1531b Fix — HTML entity rendering across 60 views
Razor's @() expression auto-encodes &, turning — into — which
rendered as literal text in the browser. Wrapped all such expressions in
@Html.Raw() so the em-dash entity is passed through unescaped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 09:27:45 -04:00

361 lines
20 KiB
Plaintext

@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@{
ViewData["Title"] = "Powder Insights";
ViewData["PageIcon"] = "bi-graph-up";
var readiness = Model.Readiness;
}
<div class="d-flex justify-content-end mb-4">
<a asp-controller="Inventory" asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-box-seam me-1"></i>Inventory
</a>
</div>
@* ── KPI cards ── *@
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-primary">@Model.ActiveJobsNeedingPowder</div>
<div class="small text-muted">Active Jobs Needing Powder</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-info">@Model.TotalEstimatedPowderNeededLbs.ToString("0.#") lbs</div>
<div class="small text-muted">Total Powder in Pipeline</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold @(Model.LowStockAlerts.Any(a => a.IsAtRisk) ? "text-danger" : "text-success")">
@Model.LowStockAlerts.Count(a => a.IsAtRisk)
</div>
<div class="small text-muted">At-Risk Stock Items</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-secondary">@readiness.JobsWithActualData</div>
<div class="small text-muted">Jobs with Actual Data</div>
</div>
</div>
</div>
</div>
@* ── Tabs ── *@
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button">
<i class="bi bi-clipboard-check me-1"></i>Stock Forecast
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="efficiency-tab" data-bs-toggle="tab" data-bs-target="#efficiency" type="button">
<i class="bi bi-speedometer2 me-1"></i>Coverage Efficiency
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="predictive-tab" data-bs-toggle="tab" data-bs-target="#predictive" type="button">
<i class="bi bi-stars me-1"></i>Predictive
@if (!readiness.IsLayer3Ready)
{
<span class="badge bg-secondary ms-1" style="font-size:.65em;">@readiness.JobsWithActualData/@readiness.Layer3MinJobs</span>
}
</button>
</li>
</ul>
<div class="tab-content">
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
<div class="tab-pane fade show active" id="forecast" role="tabpanel">
@if (!Model.LowStockAlerts.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-check-circle-fill fs-1 text-success"></i>
<p class="mt-2">No active jobs require powder, or all stock levels are sufficient.</p>
</div>
}
else
{
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock &mdash; Active Jobs</h6>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Powder</th>
<th class="text-end">On Hand</th>
<th class="text-end">Pipeline Demand</th>
<th class="text-end">Shortfall</th>
<th class="text-center">Jobs</th>
<th class="text-center">Status</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.LowStockAlerts)
{
<tr class="@(item.IsAtRisk ? "table-danger" : item.IsBelowReorderPoint ? "table-warning" : "")">
<td>
<strong>@item.Name</strong>
@if (!string.IsNullOrEmpty(item.ColorName))
{
<br /><small class="text-muted">@item.ColorName @(item.ColorCode != null ? $"({item.ColorCode})" : "") @item.Manufacturer</small>
}
</td>
<td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td>
<td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
<td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")">
@Html.Raw(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "&mdash;")
</td>
<td class="text-center">@item.ActiveJobCount</td>
<td class="text-center">
@if (item.IsAtRisk)
{
<span class="badge bg-danger"><i class="bi bi-exclamation-triangle me-1"></i>Order Now</span>
}
else if (item.IsBelowReorderPoint)
{
<span class="badge bg-warning text-dark"><i class="bi bi-arrow-down me-1"></i>Below Reorder</span>
}
else
{
<span class="badge bg-success"><i class="bi bi-check me-1"></i>OK</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
<div class="tab-pane fade" id="efficiency" role="tabpanel">
@if (!readiness.IsLayer2Ready)
{
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<i class="bi bi-hourglass-split fs-1 text-muted mb-3"></i>
<h5>Building Your Dataset</h5>
<p class="text-muted">Coverage efficiency trends become meaningful after <strong>@readiness.Layer2MinJobs jobs</strong> with actual usage recorded.</p>
<p class="text-muted mb-0">You have <strong>@readiness.JobsWithActualData</strong> so far. Keep recording actuals on completed jobs!</p>
</div>
</div>
}
else if (!Model.EfficiencyBySku.Any())
{
<div class="text-center py-5 text-muted">
<p>No efficiency data yet &mdash; record actual powder usage on completed jobs to see this chart.</p>
</div>
}
else
{
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Actual vs. Catalog Coverage Rate</h6>
<small class="text-muted">Negative variance = using more powder than spec</small>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Powder</th>
<th class="text-end">Catalog Rate</th>
<th class="text-end">Actual Avg Rate</th>
<th class="text-end">Variance</th>
<th class="text-end">Est. lbs</th>
<th class="text-end">Actual lbs</th>
<th class="text-center">Samples</th>
</tr>
</thead>
<tbody>
@foreach (var eff in Model.EfficiencyBySku)
{
<tr class="@(eff.IsBelowSpec && eff.HasEnoughData ? (eff.VariancePct < -10 ? "table-danger" : "table-warning") : "")">
<td>
<strong>@eff.Name</strong>
@if (!string.IsNullOrEmpty(eff.ColorName))
{
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
}
@if (!eff.HasEnoughData)
{
<br /><small class="text-muted fst-italic">Low confidence &mdash; need 5+ samples</small>
}
</td>
<td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
<td class="text-end fw-semibold">@eff.ActualAvgCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
<td class="text-end @(eff.VariancePct < 0 ? "text-danger" : "text-success")">
@(eff.VariancePct > 0 ? "+" : "")@eff.VariancePct.ToString("0.#")%
</td>
<td class="text-end text-muted">@eff.TotalEstimatedLbs.ToString("0.#")</td>
<td class="text-end">@eff.TotalActualLbs.ToString("0.#")</td>
<td class="text-center">
<span class="badge @(eff.HasEnoughData ? "bg-primary" : "bg-secondary")">@eff.SampleCount</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
<div class="tab-pane fade" id="predictive" role="tabpanel">
@if (!readiness.IsLayer3Ready)
{
@* Progress gate *@
<div class="card border-0 shadow-sm">
<div class="card-body py-5">
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<i class="bi bi-lock fs-1 text-secondary mb-3"></i>
<h5>Predictive Insights Unlocks at @readiness.Layer3MinJobs Jobs</h5>
<p class="text-muted">Once @readiness.Layer3MinJobs jobs have actual powder usage recorded, this tab will show AI-powered reorder suggestions and waste pattern analysis.</p>
<div class="progress mb-2" style="height: 12px;">
<div class="progress-bar bg-primary progress-bar-striped progress-bar-animated"
style="width: @readiness.Layer3ProgressPercent%">
</div>
</div>
<p class="text-muted small mb-4">
<strong>@readiness.JobsWithActualData</strong> of <strong>@readiness.Layer3MinJobs</strong> jobs recorded
(@readiness.Layer3ProgressPercent%)
</p>
<div class="alert alert-info alert-permanent text-start">
<h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6>
<ul class="mb-0 small">
<li><strong>Smart reorder suggestions</strong> &mdash; quantity recommendations based on your actual usage history + scheduled job pipeline</li>
<li><strong>Waste pattern detection</strong> &mdash; identifies jobs and powder types that consistently over-consume</li>
<li><strong>Per-powder efficiency corrections</strong> &mdash; suggests updating coverage defaults based on real data</li>
</ul>
</div>
</div>
</div>
</div>
</div>
}
else
{
@* Reorder Suggestions *@
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-transparent d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-cart-plus me-2"></i>Smart Reorder Suggestions</h6>
<small class="text-muted">Based on @readiness.JobsWithActualData jobs of actual data + 30-day pipeline</small>
</div>
@if (!Model.ReorderSuggestions.Any())
{
<div class="card-body text-muted text-center py-4">No reorder suggestions &mdash; stock levels look good for upcoming pipeline.</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Powder</th>
<th class="text-end">On Hand</th>
<th class="text-end">30-Day Pipeline</th>
<th class="text-end">Avg Monthly Usage</th>
<th class="text-end">Suggested Order</th>
<th class="text-center">Confidence</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.ReorderSuggestions)
{
var confPct = (int)(s.ConfidenceScore * 100);
var confClass = confPct >= 70 ? "success" : confPct >= 40 ? "warning" : "secondary";
<tr>
<td>
<strong>@s.Name</strong>
@if (!string.IsNullOrEmpty(s.ColorName))
{
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
}
</td>
<td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
<td class="text-end">@s.PipelineDemand30DaysLbs.ToString("0.#") lbs</td>
<td class="text-end">@s.HistoricalAvgMonthlyUsageLbs.ToString("0.#") lbs</td>
<td class="text-end fw-bold text-primary">@s.SuggestedOrderQtyLbs.ToString("0.#") lbs</td>
<td class="text-center">
<div class="d-flex align-items-center justify-content-center gap-1">
<div class="progress flex-grow-1" style="height:8px; max-width:60px;">
<div class="progress-bar bg-@confClass" style="width:@confPct%"></div>
</div>
<small class="text-muted">@confPct%</small>
</div>
<div class="text-muted" style="font-size:.7em;">@s.SampleJobCount jobs</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
@* Waste Patterns *@
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal">&mdash; coats that used &gt;20% more than estimated</small></h6>
</div>
@if (!Model.WastePatterns.Any())
{
<div class="card-body text-muted text-center py-4">
<i class="bi bi-check-circle-fill text-success me-2"></i>No significant waste patterns detected. Great accuracy!
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Job</th>
<th>Item / Coat</th>
<th>Powder</th>
<th>Complexity</th>
<th class="text-end">Estimated</th>
<th class="text-end">Actual</th>
<th class="text-end">Overage</th>
</tr>
</thead>
<tbody>
@foreach (var w in Model.WastePatterns)
{
<tr>
<td><a asp-controller="Jobs" asp-action="Details" asp-route-id="@w.JobId">@w.JobNumber</a></td>
<td>
@w.ItemDescription
<br /><small class="text-muted">@w.CoatName</small>
</td>
<td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td>
<td>@Html.Raw(w.Complexity ?? "&mdash;")</td>
<td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td>
<td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td>
<td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
</div>
</div>