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>
258 lines
10 KiB
Plaintext
258 lines
10 KiB
Plaintext
@page "/projects/{Id:int}/results"
|
|
@inject ProjectService ProjectService
|
|
@inject CutListPackingService PackingService
|
|
@inject NavigationManager Navigation
|
|
@inject IJSRuntime JS
|
|
@using CutList.Core
|
|
@using CutList.Core.Nesting
|
|
@using CutList.Core.Formatting
|
|
|
|
<PageTitle>Results - @(project?.Name ?? "Project")</PageTitle>
|
|
|
|
@if (loading)
|
|
{
|
|
<p><em>Loading...</em></p>
|
|
}
|
|
else if (project == null)
|
|
{
|
|
<div class="alert alert-danger">Project not found.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h1>@project.Name</h1>
|
|
@if (!string.IsNullOrWhiteSpace(project.Customer))
|
|
{
|
|
<p class="text-muted mb-0">Customer: @project.Customer</p>
|
|
}
|
|
</div>
|
|
<div>
|
|
<a href="projects/@Id" class="btn btn-outline-secondary me-2">Edit Project</a>
|
|
<button class="btn btn-primary" @onclick="PrintReport">Print Report</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!CanOptimize)
|
|
{
|
|
<div class="alert alert-warning">
|
|
<h4>Cannot Optimize</h4>
|
|
<ul class="mb-0">
|
|
@if (project.Parts.Count == 0)
|
|
{
|
|
<li>No parts defined. <a href="projects/@Id">Add parts to the project</a>.</li>
|
|
}
|
|
@if (project.CuttingToolId == null)
|
|
{
|
|
<li>No cutting tool selected. <a href="projects/@Id">Select a cutting tool</a>.</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
else if (packResult != null)
|
|
{
|
|
@if (summary!.TotalItemsNotPlaced > 0)
|
|
{
|
|
<div class="alert alert-warning">
|
|
<h5>Items Not Placed</h5>
|
|
<p>Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
|
|
</div>
|
|
}
|
|
|
|
<!-- Overall Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3 col-6 mb-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
|
|
<p class="card-text text-muted">Total Stock Bars</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6 mb-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-0">@summary.TotalPieces</h2>
|
|
<p class="card-text text-muted">Total Pieces</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6 mb-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
|
|
<p class="card-text text-muted">Total Waste</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6 mb-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-0">@summary.Efficiency.ToString("F1")%</h2>
|
|
<p class="card-text text-muted">Efficiency</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stock Summary -->
|
|
<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>
|
|
}
|
|
}
|
|
}
|
|
|
|
@code {
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
private Project? project;
|
|
private MultiMaterialPackResult? packResult;
|
|
private MultiMaterialPackingSummary? summary;
|
|
private bool loading = true;
|
|
|
|
private bool CanOptimize => project != null &&
|
|
project.Parts.Count > 0 &&
|
|
project.CuttingToolId != null;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
project = await ProjectService.GetByIdAsync(Id);
|
|
|
|
if (project != null && CanOptimize)
|
|
{
|
|
var kerf = project.CuttingTool?.KerfInches ?? 0.125m;
|
|
packResult = await PackingService.PackAsync(project.Parts, kerf);
|
|
summary = PackingService.GetSummary(packResult);
|
|
}
|
|
|
|
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()
|
|
{
|
|
var filename = $"CutList - {project!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
|
await JS.InvokeVoidAsync("printWithTitle", filename);
|
|
}
|
|
}
|