refactor: Merge Results page into Job Edit as a tab

Move optimization results UI from separate Results.razor page into
the Edit.razor tabbed editor. Results are now loaded from saved JSON
on page load instead of re-running on every visit. Remove the
standalone optimize button from Jobs index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 22:12:57 -05:00
parent 891b214b29
commit 59f86c8e79
3 changed files with 406 additions and 363 deletions

View File

@@ -3,7 +3,12 @@
@inject JobService JobService
@inject MaterialService MaterialService
@inject StockItemService StockItemService
@inject CutListPackingService PackingService
@inject PurchaseItemService PurchaseItemService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@using CutList.Core
@using CutList.Core.Nesting
@using CutList.Core.Formatting
@using CutList.Web.Data.Entities
@@ -11,10 +16,7 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>@(IsNew ? "New Job" : job.DisplayName)</h1>
@if (!IsNew)
{
<a href="jobs/@Id/results" class="btn btn-success">Run Optimization</a>
}
<a href="jobs" class="btn btn-outline-secondary">Back to Jobs</a>
</div>
@if (!IsNew && job.IsLocked)
@@ -73,6 +75,16 @@ else
}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == Tab.Results ? "active" : "")"
@onclick="() => SetTab(Tab.Results)" type="button">
Results
@if (summary != null)
{
<span class="badge bg-success ms-1">@summary.Efficiency.ToString("F0")%</span>
}
</button>
</li>
</ul>
<div class="tab-content">
@@ -92,6 +104,10 @@ else
{
@RenderStockTab()
}
else if (activeTab == Tab.Results)
{
@RenderResultsTab()
}
</div>
}
@@ -253,7 +269,7 @@ else
}
@code {
private enum Tab { Details, Parts, Stock }
private enum Tab { Details, Parts, Stock, Results }
[Parameter]
public int? Id { get; set; }
@@ -292,12 +308,20 @@ else
private List<ImportStockCandidate> importCandidates = new();
private string? importErrorMessage;
// Results tab
private MultiMaterialPackResult? packResult;
private MultiMaterialPackingSummary? summary;
private bool optimizing;
private bool addingToOrderList;
private bool addedToOrderList;
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
? Enumerable.Empty<Material>()
: materials.Where(m => m.Shape == selectedShape.Value).OrderBy(m => m.SortOrder).ThenBy(m => m.Size);
private bool IsNew => !Id.HasValue;
private bool CanOptimize => job.Parts.Count > 0 && job.CuttingToolId != null;
private async Task UnlockJob()
{
@@ -322,6 +346,12 @@ else
return;
}
job = existing;
// Load saved optimization results if available
if (job.OptimizationResultJson != null)
{
LoadSavedResults();
}
}
else
{
@@ -336,6 +366,25 @@ else
loading = false;
}
private void LoadSavedResults()
{
try
{
packResult = PackingService.LoadSavedResult(job.OptimizationResultJson!);
if (packResult != null)
{
summary = PackingService.GetSummary(packResult);
addedToOrderList = job.IsLocked;
}
}
catch
{
// Invalid JSON — treat as no results
packResult = null;
summary = null;
}
}
private RenderFragment RenderDetailsForm() => __builder =>
{
<div class="card">
@@ -474,6 +523,10 @@ else
else
{
await JobService.UpdateAsync(job);
job = (await JobService.GetByIdAsync(Id!.Value))!;
// Clear in-memory results since they were invalidated
packResult = null;
summary = null;
}
}
finally
@@ -561,12 +614,17 @@ else
job = (await JobService.GetByIdAsync(Id!.Value))!;
showPartForm = false;
editingPart = null;
// Results were cleared by the service
packResult = null;
summary = null;
}
private async Task DeletePart(JobPart part)
{
await JobService.DeletePartAsync(part.Id);
job = (await JobService.GetByIdAsync(Id!.Value))!;
packResult = null;
summary = null;
}
// Stock tab
@@ -582,10 +640,7 @@ else
title="@(job.Parts.Count == 0 ? "Add parts first to match against inventory" : "Find and import stock matching your parts")">
Import from Inventory
</button>
<div class="btn-group">
<button class="btn btn-primary" @onclick="ShowAddStockFromInventory">Add from Inventory</button>
<button class="btn btn-outline-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
</div>
<button class="btn btn-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
</div>
}
</div>
@@ -777,16 +832,341 @@ else
</div>
};
private void ShowAddStockFromInventory()
// Results tab
private RenderFragment RenderResultsTab() => __builder =>
{
editingStock = null;
newStock = new JobStock { JobId = Id!.Value, Quantity = 1, Priority = 10 };
stockSelectedShape = null;
stockSelectedMaterialId = 0;
availableStockItems.Clear();
showStockForm = true;
showCustomStockForm = false;
stockErrorMessage = null;
@if (!CanOptimize)
{
<div class="alert alert-warning">
<h5 class="mb-2">Cannot Optimize</h5>
<ul class="mb-0">
@if (job.Parts.Count == 0)
{
<li>No parts defined. Switch to the <button class="btn btn-link p-0" @onclick="() => SetTab(Tab.Parts)">Parts tab</button> to add parts.</li>
}
@if (job.CuttingToolId == null)
{
<li>No cutting tool selected. Switch to the <button class="btn btn-link p-0" @onclick="() => SetTab(Tab.Details)">Details tab</button> to select a cutting tool.</li>
}
</ul>
</div>
}
else
{
<div class="mb-3">
<button class="btn btn-success" @onclick="RunOptimization" disabled="@(optimizing || job.IsLocked)">
@if (optimizing)
{
<span class="spinner-border spinner-border-sm me-1"></span>
<text>Optimizing...</text>
}
else if (packResult != null)
{
<i class="bi bi-arrow-clockwise me-1"></i>
<text>Re-Optimize</text>
}
else
{
<i class="bi bi-scissors me-1"></i>
<text>Optimize</text>
}
</button>
@if (packResult != null)
{
<button class="btn btn-outline-secondary ms-2" @onclick="PrintReport">
<i class="bi bi-printer me-1"></i> Print Report
</button>
}
@if (job.OptimizedAt.HasValue)
{
<span class="text-muted ms-3">
Last optimized: @job.OptimizedAt.Value.ToLocalTime().ToString("g")
</span>
}
</div>
@if (packResult != null && summary != null)
{
@if (summary.TotalItemsNotPlaced > 0)
{
<div class="alert alert-warning">
<h5>Items Not Placed</h5>
<p class="mb-0">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 print-summary">
<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 print-stock-summary">
<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>
@if (summary.TotalToBePurchasedBins > 0)
{
@if (addedToOrderList)
{
<div class="alert alert-success mb-0 mt-2 py-2">
Added to order list. <a href="orders">View Orders</a>
</div>
}
else
{
<button class="btn btn-warning btn-sm mt-2" @onclick="AddToOrderList" disabled="@addingToOrderList">
@if (addingToOrderList)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
<i class="bi bi-cart-plus"></i> Add to Order List
</button>
}
}
else
{
<p class="text-muted mb-0">Everything available in stock</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>
}
}
else if (!optimizing)
{
<div class="text-center py-5 text-muted">
<i class="bi bi-scissors display-4"></i>
<p class="mt-3">Click <strong>Optimize</strong> to calculate the most efficient cut list.</p>
</div>
}
}
};
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 RunOptimization()
{
optimizing = true;
try
{
var kerf = job.CuttingTool?.KerfInches ?? 0.125m;
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
summary = PackingService.GetSummary(packResult);
// Save to database
var json = PackingService.SerializeResult(packResult);
await JobService.SaveOptimizationResultAsync(Id!.Value, json, DateTime.UtcNow);
// Refresh job to get updated OptimizedAt
job = (await JobService.GetByIdAsync(Id!.Value))!;
addedToOrderList = job.IsLocked;
}
finally
{
optimizing = false;
}
}
private async Task AddToOrderList()
{
addingToOrderList = true;
try
{
var purchaseItems = new List<PurchaseItem>();
var stockItems = await StockItemService.GetAllAsync();
foreach (var materialResult in packResult!.MaterialResults)
{
if (materialResult.ToBePurchasedBins.Count == 0) continue;
var materialId = materialResult.Material.Id;
// Group bins by length to consolidate quantities
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
{
var lengthInches = (decimal)group.Key;
var quantity = group.Count();
// Find the matching stock item
var stockItem = stockItems.FirstOrDefault(s =>
s.MaterialId == materialId && s.LengthInches == lengthInches);
if (stockItem != null)
{
purchaseItems.Add(new PurchaseItem
{
StockItemId = stockItem.Id,
Quantity = quantity,
JobId = Id!.Value,
Status = PurchaseItemStatus.Pending
});
}
}
}
if (purchaseItems.Count > 0)
{
await PurchaseItemService.CreateBulkAsync(purchaseItems);
}
await JobService.LockAsync(Id!.Value);
job = (await JobService.GetByIdAsync(Id!.Value))!;
addedToOrderList = true;
}
finally
{
addingToOrderList = false;
}
}
private async Task PrintReport()
{
var filename = $"CutList - {job.Name} - {DateTime.Now:yyyy-MM-dd}";
await JS.InvokeVoidAsync("printWithTitle", filename);
}
private void ShowAddCustomStock()
@@ -911,6 +1291,8 @@ else
job = (await JobService.GetByIdAsync(Id!.Value))!;
showStockForm = false;
editingStock = null;
packResult = null;
summary = null;
}
private async Task SaveCustomStockAsync()
@@ -956,12 +1338,16 @@ else
job = (await JobService.GetByIdAsync(Id!.Value))!;
showCustomStockForm = false;
editingStock = null;
packResult = null;
summary = null;
}
private async Task DeleteStock(JobStock stock)
{
await JobService.DeleteStockAsync(stock.Id);
job = (await JobService.GetByIdAsync(Id!.Value))!;
packResult = null;
summary = null;
}
// Import modal methods
@@ -1048,6 +1434,8 @@ else
job = (await JobService.GetByIdAsync(Id!.Value))!;
showImportModal = false;
importCandidates.Clear();
packResult = null;
summary = null;
}
catch (Exception ex)
{

View File

@@ -63,7 +63,6 @@ else
<td>@((job.UpdatedAt ?? job.CreatedAt).ToLocalTime().ToString("g"))</td>
<td>
<a href="jobs/@job.Id" class="btn btn-sm btn-outline-primary" title="Edit"><i class="bi bi-pencil"></i></a>
<a href="jobs/@job.Id/results" class="btn btn-sm btn-success" title="Optimize"><i class="bi bi-scissors"></i></a>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateJob(job)" title="Copy"><i class="bi bi-copy"></i></button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(job)" title="Delete"><i class="bi bi-trash"></i></button>
</td>

View File

@@ -1,344 +0,0 @@
@page "/jobs/{Id:int}/results"
@inject JobService JobService
@inject CutListPackingService PackingService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject PurchaseItemService PurchaseItemService
@inject StockItemService StockItemService
@using CutList.Core
@using CutList.Core.Nesting
@using CutList.Core.Formatting
<PageTitle>Results - @(job?.DisplayName ?? "Job")</PageTitle>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (job == null)
{
<div class="alert alert-danger">Job not found.</div>
}
else
{
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h1>
@job.DisplayName
@if (job.IsLocked)
{
<i class="bi bi-lock-fill text-warning ms-2" title="Job locked — materials ordered"></i>
}
</h1>
@if (!string.IsNullOrWhiteSpace(job.Customer))
{
<p class="text-muted mb-0">Customer: @job.Customer</p>
}
</div>
<div>
<a href="jobs/@Id" class="btn btn-outline-secondary me-2">Edit Job</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 (job.Parts.Count == 0)
{
<li>No parts defined. <a href="jobs/@Id">Add parts to the job</a>.</li>
}
@if (job.CuttingToolId == null)
{
<li>No cutting tool selected. <a href="jobs/@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 print-summary">
<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 print-stock-summary">
<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>
@if (summary.TotalToBePurchasedBins > 0)
{
@if (addedToOrderList)
{
<div class="alert alert-success mb-0 mt-2 py-2">
Added to order list. <a href="orders">View Orders</a>
</div>
}
else
{
<button class="btn btn-warning btn-sm mt-2" @onclick="AddToOrderList" disabled="@addingToOrderList">
@if (addingToOrderList)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
<i class="bi bi-cart-plus"></i> Add to Order List
</button>
}
}
else
{
<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 Job? job;
private MultiMaterialPackResult? packResult;
private MultiMaterialPackingSummary? summary;
private bool loading = true;
private bool addingToOrderList;
private bool addedToOrderList;
private bool CanOptimize => job != null &&
job.Parts.Count > 0 &&
job.CuttingToolId != null;
protected override async Task OnInitializedAsync()
{
job = await JobService.GetByIdAsync(Id);
if (job != null && CanOptimize)
{
var kerf = job.CuttingTool?.KerfInches ?? 0.125m;
// Pass job stock if configured, otherwise packing service uses all available stock
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
summary = PackingService.GetSummary(packResult);
addedToOrderList = job.IsLocked;
}
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 AddToOrderList()
{
addingToOrderList = true;
try
{
var purchaseItems = new List<PurchaseItem>();
var stockItems = await StockItemService.GetAllAsync();
foreach (var materialResult in packResult!.MaterialResults)
{
if (materialResult.ToBePurchasedBins.Count == 0) continue;
var materialId = materialResult.Material.Id;
// Group bins by length to consolidate quantities
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
{
var lengthInches = (decimal)group.Key;
var quantity = group.Count();
// Find the matching stock item
var stockItem = stockItems.FirstOrDefault(s =>
s.MaterialId == materialId && s.LengthInches == lengthInches);
if (stockItem != null)
{
purchaseItems.Add(new PurchaseItem
{
StockItemId = stockItem.Id,
Quantity = quantity,
JobId = Id,
Status = PurchaseItemStatus.Pending
});
}
}
}
if (purchaseItems.Count > 0)
{
await PurchaseItemService.CreateBulkAsync(purchaseItems);
}
await JobService.LockAsync(Id);
job = await JobService.GetByIdAsync(Id);
addedToOrderList = true;
}
finally
{
addingToOrderList = false;
}
}
private async Task PrintReport()
{
var filename = $"CutList - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
await JS.InvokeVoidAsync("printWithTitle", filename);
}
}