Initial commit
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
@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 btn-sm">
|
||||
<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 — 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")">
|
||||
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "—")
|
||||
</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 — 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 — 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 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> — quantity recommendations based on your actual usage history + scheduled job pipeline</li>
|
||||
<li><strong>Waste pattern detection</strong> — identifies jobs and powder types that consistently over-consume</li>
|
||||
<li><strong>Per-powder efficiency corrections</strong> — 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 — 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">— coats that used >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>@(w.Complexity ?? "—")</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>
|
||||
@@ -0,0 +1,74 @@
|
||||
@model PowderCoating.Application.DTOs.Powder.JobPowderSummaryDto
|
||||
|
||||
@if (!Model.Coats.Any())
|
||||
{
|
||||
<p class="text-muted small">No coats with powder estimates on this job.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr class="table-light">
|
||||
<th>Item / Coat</th>
|
||||
<th class="text-end">Est.</th>
|
||||
<th class="text-end">Actual</th>
|
||||
<th class="text-end">Var.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var coat in Model.Coats)
|
||||
{
|
||||
<tr>
|
||||
<td class="small">
|
||||
<span class="text-muted">@coat.ItemDescription</span>
|
||||
<br />@coat.CoatName
|
||||
@if (!string.IsNullOrEmpty(coat.ColorName))
|
||||
{
|
||||
<span class="text-muted"> · @coat.ColorName</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end small">@(coat.EstimatedLbs.HasValue ? $"{coat.EstimatedLbs:0.##}" : "—")</td>
|
||||
<td class="text-end small">
|
||||
@if (coat.IsRecorded)
|
||||
{
|
||||
<span class="text-success fw-semibold">@coat.ActualLbs!.Value.ToString("0.##")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted fst-italic">pending</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end small">
|
||||
@if (coat.VarianceLbs.HasValue)
|
||||
{
|
||||
var cls = coat.VarianceLbs > 0 ? "text-danger" : "text-success";
|
||||
var sign = coat.VarianceLbs > 0 ? "+" : "";
|
||||
<span class="@cls">@sign@coat.VarianceLbs.Value.ToString("0.##")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@if (Model.Coats.Count > 1)
|
||||
{
|
||||
<tfoot>
|
||||
<tr class="fw-semibold table-light">
|
||||
<td>Total</td>
|
||||
<td class="text-end">@Model.TotalEstimatedLbs.ToString("0.##") lbs</td>
|
||||
<td class="text-end @(Model.TotalActualLbs > 0 ? "text-success" : "")">
|
||||
@(Model.TotalActualLbs > 0 ? $"{Model.TotalActualLbs:0.##} lbs" : "—")
|
||||
</td>
|
||||
<td class="text-end @(Model.TotalVarianceLbs > 0 ? "text-danger" : Model.TotalVarianceLbs < 0 ? "text-success" : "")">
|
||||
@(Model.TotalActualLbs > 0 ? $"{(Model.TotalVarianceLbs > 0 ? "+" : "")}{Model.TotalVarianceLbs:0.##} lbs" : "—")
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user