feat: Redesign Results page for multi-material output

Updates Results page to display packing results grouped by material,
showing in-stock vs. to-be-purchased breakdown with order summaries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 23:56:28 -05:00
parent c99de55fe1
commit ed911a13ba
2 changed files with 141 additions and 26 deletions

View File

@@ -1,9 +1,9 @@
@page "/projects/{Id:int}/results" @page "/projects/{Id:int}/results"
@inject ProjectService ProjectService @inject ProjectService ProjectService
@inject CutListPackingService PackingService @inject CutListPackingService PackingService
@inject ReportService ReportService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IJSRuntime JS @inject IJSRuntime JS
@using CutList.Core
@using CutList.Core.Nesting @using CutList.Core.Nesting
@using CutList.Core.Formatting @using CutList.Core.Formatting
@@ -22,7 +22,10 @@ else
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<div> <div>
<h1>@project.Name</h1> <h1>@project.Name</h1>
<p class="text-muted mb-0">Optimization Results</p> @if (!string.IsNullOrWhiteSpace(project.Customer))
{
<p class="text-muted mb-0">Customer: @project.Customer</p>
}
</div> </div>
<div> <div>
<a href="projects/@Id" class="btn btn-outline-secondary me-2">Edit Project</a> <a href="projects/@Id" class="btn btn-outline-secondary me-2">Edit Project</a>
@@ -39,10 +42,6 @@ else
{ {
<li>No parts defined. <a href="projects/@Id">Add parts to the project</a>.</li> <li>No parts defined. <a href="projects/@Id">Add parts to the project</a>.</li>
} }
@if (project.StockBins.Count == 0)
{
<li>No stock bins defined. <a href="projects/@Id">Add stock bins to the project</a>.</li>
}
@if (project.CuttingToolId == null) @if (project.CuttingToolId == null)
{ {
<li>No cutting tool selected. <a href="projects/@Id">Select a cutting tool</a>.</li> <li>No cutting tool selected. <a href="projects/@Id">Select a cutting tool</a>.</li>
@@ -52,27 +51,21 @@ else
} }
else if (packResult != null) else if (packResult != null)
{ {
@if (packResult.ItemsNotUsed.Count > 0) @if (summary!.TotalItemsNotPlaced > 0)
{ {
<div class="alert alert-warning"> <div class="alert alert-warning">
<h5>Items Not Placed</h5> <h5>Items Not Placed</h5>
<p>The following @packResult.ItemsNotUsed.Count item(s) could not be placed (probably too long for available stock):</p> <p>Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
<ul class="mb-0">
@foreach (var item in packResult.ItemsNotUsed.GroupBy(i => new { i.Name, i.Length }))
{
<li>@item.Count() x @item.Key.Name (@ArchUnits.FormatFromInches(item.Key.Length))</li>
}
</ul>
</div> </div>
} }
<!-- Summary Cards --> <!-- Overall Summary Cards -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-3 col-6 mb-3"> <div class="col-md-3 col-6 mb-3">
<div class="card text-center"> <div class="card text-center">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-0">@summary!.TotalBins</h2> <h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
<p class="card-text text-muted">Stock Bars</p> <p class="card-text text-muted">Total Stock Bars</p>
</div> </div>
</div> </div>
</div> </div>
@@ -102,8 +95,94 @@ else
</div> </div>
</div> </div>
<!-- Report --> <!-- Stock Summary -->
<CutListReport Project="project" PackResult="packResult" /> <div class="row mb-4">
<div class="col-md-6 mb-3">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0">In Stock</h5>
</div>
<div class="card-body">
<h3>@summary.TotalInStockBins bars</h3>
<p class="text-muted mb-0">Ready to cut from existing inventory</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card border-warning">
<div class="card-header bg-warning">
<h5 class="mb-0">To Be Purchased</h5>
</div>
<div class="card-body">
<h3>@summary.TotalToBePurchasedBins bars</h3>
<p class="text-muted mb-0">Need to order from supplier</p>
</div>
</div>
</div>
</div>
<!-- Results by Material -->
@foreach (var materialResult in packResult.MaterialResults)
{
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
<div class="card mb-4">
<div class="card-header">
<h4 class="mb-0">@materialResult.Material.DisplayName</h4>
</div>
<div class="card-body">
<!-- Material Summary -->
<div class="row mb-3">
<div class="col-md-2 col-4">
<strong>@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins)</strong> bars
</div>
<div class="col-md-2 col-4">
<strong>@materialSummary.TotalPieces</strong> pieces
</div>
<div class="col-md-2 col-4">
<strong>@materialSummary.Efficiency.ToString("F1")%</strong> efficiency
</div>
<div class="col-md-3 col-6">
<span class="text-success">@materialSummary.InStockBins in stock</span>
</div>
<div class="col-md-3 col-6">
<span class="text-warning">@materialSummary.ToBePurchasedBins to purchase</span>
</div>
</div>
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
{
<div class="alert alert-danger">
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> -
No stock lengths available or parts too long.
</div>
}
@if (materialResult.InStockBins.Count > 0)
{
<h5 class="text-success mt-3">In Stock (@materialResult.InStockBins.Count bars)</h5>
@RenderBinList(materialResult.InStockBins)
}
@if (materialResult.ToBePurchasedBins.Count > 0)
{
<h5 class="text-warning mt-3">To Be Purchased (@materialResult.ToBePurchasedBins.Count bars)</h5>
@RenderBinList(materialResult.ToBePurchasedBins)
<!-- Purchase Summary -->
<div class="mt-3 p-3 bg-light rounded">
<strong>Order Summary:</strong>
<ul class="mb-0 mt-2">
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
{
<li>@group.Count() x @ArchUnits.FormatFromInches(group.Key)</li>
}
</ul>
</div>
}
</div>
</div>
}
} }
} }
@@ -112,13 +191,12 @@ else
public int Id { get; set; } public int Id { get; set; }
private Project? project; private Project? project;
private PackResult? packResult; private MultiMaterialPackResult? packResult;
private PackingSummary? summary; private MultiMaterialPackingSummary? summary;
private bool loading = true; private bool loading = true;
private bool CanOptimize => project != null && private bool CanOptimize => project != null &&
project.Parts.Count > 0 && project.Parts.Count > 0 &&
project.StockBins.Count > 0 &&
project.CuttingToolId != null; project.CuttingToolId != null;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -128,15 +206,52 @@ else
if (project != null && CanOptimize) if (project != null && CanOptimize)
{ {
var kerf = project.CuttingTool?.KerfInches ?? 0.125m; var kerf = project.CuttingTool?.KerfInches ?? 0.125m;
packResult = PackingService.Pack(project.Parts, project.StockBins, kerf); packResult = await PackingService.PackAsync(project.Parts, kerf);
summary = PackingService.GetSummary(packResult); summary = PackingService.GetSummary(packResult);
} }
loading = false; loading = false;
} }
private RenderFragment RenderBinList(List<Bin> bins) => __builder =>
{
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th style="width: 80px;">#</th>
<th>Stock Length</th>
<th>Cuts</th>
<th>Waste</th>
</tr>
</thead>
<tbody>
@{ var binNumber = 1; }
@foreach (var bin in bins)
{
<tr>
<td>@binNumber</td>
<td>@ArchUnits.FormatFromInches(bin.Length)</td>
<td>
@foreach (var item in bin.Items)
{
<span class="badge bg-primary me-1">
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
</span>
}
</td>
<td>@ArchUnits.FormatFromInches(bin.RemainingLength)</td>
</tr>
binNumber++;
}
</tbody>
</table>
</div>
};
private async Task PrintReport() private async Task PrintReport()
{ {
await JS.InvokeVoidAsync("window.print"); var filename = $"CutList - {project!.Name} - {DateTime.Now:yyyy-MM-dd}";
await JS.InvokeVoidAsync("printWithTitle", filename);
} }
} }

View File

@@ -9,9 +9,9 @@
<div class="meta-info"> <div class="meta-info">
<div class="meta-row"><span>Date:</span> @DateTime.Now.ToString("g")</div> <div class="meta-row"><span>Date:</span> @DateTime.Now.ToString("g")</div>
<div class="meta-row"><span>Project:</span> @Project.Name</div> <div class="meta-row"><span>Project:</span> @Project.Name</div>
@if (Project.Material != null) @if (!string.IsNullOrWhiteSpace(Project.Customer))
{ {
<div class="meta-row"><span>Material:</span> @Project.Material.Shape - @Project.Material.Size</div> <div class="meta-row"><span>Customer:</span> @Project.Customer</div>
} }
@if (Project.CuttingTool != null) @if (Project.CuttingTool != null)
{ {