@page "/jobs/new" @page "/jobs/{Id:int}" @inject JobService JobService @inject MaterialService MaterialService @inject StockItemService StockItemService @inject NavigationManager Navigation @using CutList.Core.Formatting @using CutList.Web.Data.Entities @(IsNew ? "New Job" : job.DisplayName)

@(IsNew ? "New Job" : job.DisplayName)

@if (!IsNew) { Run Optimization }
@if (loading) {

Loading...

} else if (IsNew) {
@RenderDetailsForm()
} else {
@if (activeTab == Tab.Details) {
@RenderDetailsForm()
} else if (activeTab == Tab.Parts) { @RenderPartsTab() } else if (activeTab == Tab.Stock) { @RenderStockTab() }
} @code { private enum Tab { Details, Parts, Stock } [Parameter] public int? Id { get; set; } private Job job = new(); private List materials = new(); private List cuttingTools = new(); private bool loading = true; private bool savingJob; private string? jobErrorMessage; private Tab activeTab = Tab.Details; private void SetTab(Tab tab) => activeTab = tab; // Parts form private bool showPartForm; private JobPart newPart = new(); private JobPart? editingPart; private string? partErrorMessage; private MaterialShape? selectedShape; // Stock form private bool showStockForm; private bool showCustomStockForm; private JobStock newStock = new(); private JobStock? editingStock; private string? stockErrorMessage; private MaterialShape? stockSelectedShape; private int stockSelectedMaterialId; private List availableStockItems = new(); private IEnumerable DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s); private IEnumerable FilteredMaterials => !selectedShape.HasValue ? Enumerable.Empty() : materials.Where(m => m.Shape == selectedShape.Value).OrderBy(m => m.Size); private bool IsNew => !Id.HasValue; protected override async Task OnInitializedAsync() { materials = await MaterialService.GetAllAsync(); cuttingTools = await JobService.GetCuttingToolsAsync(); if (Id.HasValue) { var existing = await JobService.GetByIdAsync(Id.Value); if (existing == null) { Navigation.NavigateTo("jobs"); return; } job = existing; } else { // Set default cutting tool for new jobs var defaultTool = await JobService.GetDefaultCuttingToolAsync(); if (defaultTool != null) { job.CuttingToolId = defaultTool.Id; } } loading = false; } private RenderFragment RenderDetailsForm() => __builder => {
Job Details
@if (!IsNew) {
}
@foreach (var tool in cuttingTools) { }
@if (!string.IsNullOrEmpty(jobErrorMessage)) {
@jobErrorMessage
}
Back
}; private RenderFragment RenderPartsTab() => __builder => {
Parts to Cut
@if (showPartForm) {
@(editingPart == null ? "Add Part" : "Edit Part")
@if (!string.IsNullOrEmpty(partErrorMessage)) {
@partErrorMessage
}
} @if (job.Parts.Count == 0) {

No parts added yet.

Add the parts you need to cut, selecting the material for each.

} else {
@foreach (var part in job.Parts) { }
Material Length Qty Name Actions
@part.Material.DisplayName @ArchUnits.FormatFromInches((double)part.LengthInches) @part.Quantity @(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)
Total: @job.Parts.Sum(p => p.Quantity) pieces
}
}; private async Task SaveJobAsync() { jobErrorMessage = null; savingJob = true; try { if (IsNew) { var created = await JobService.CreateAsync(job); Navigation.NavigateTo($"jobs/{created.Id}"); } else { await JobService.UpdateAsync(job); } } finally { savingJob = false; } } // Parts methods private void ShowAddPartForm() { editingPart = null; newPart = new JobPart { JobId = Id!.Value, Quantity = 1 }; selectedShape = null; showPartForm = true; partErrorMessage = null; } private void OnShapeChanged() { newPart.MaterialId = 0; } private void EditPart(JobPart part) { editingPart = part; newPart = new JobPart { Id = part.Id, JobId = part.JobId, MaterialId = part.MaterialId, Name = part.Name, LengthInches = part.LengthInches, Quantity = part.Quantity, SortOrder = part.SortOrder }; selectedShape = part.Material?.Shape; showPartForm = true; partErrorMessage = null; } private void CancelPartForm() { showPartForm = false; editingPart = null; } private async Task SavePartAsync() { partErrorMessage = null; if (!selectedShape.HasValue) { partErrorMessage = "Please select a shape"; return; } if (newPart.MaterialId == 0) { partErrorMessage = "Please select a size"; return; } if (newPart.LengthInches <= 0) { partErrorMessage = "Length must be greater than zero"; return; } if (newPart.Quantity < 1) { partErrorMessage = "Quantity must be at least 1"; return; } if (editingPart == null) { await JobService.AddPartAsync(newPart); } else { await JobService.UpdatePartAsync(newPart); } job = (await JobService.GetByIdAsync(Id!.Value))!; showPartForm = false; editingPart = null; } private async Task DeletePart(JobPart part) { await JobService.DeletePartAsync(part.Id); job = (await JobService.GetByIdAsync(Id!.Value))!; } // Stock tab private RenderFragment RenderStockTab() => __builder => {
Stock for This Job
@if (showStockForm) { @RenderStockFromInventoryForm() } else if (showCustomStockForm) { @RenderCustomStockForm() } @if (job.Stock.Count == 0) {

No stock configured for this job.

Add stock from your inventory or define custom lengths.

If no stock is selected, the optimizer will use all available stock for the materials in your parts list.

} else { @RenderStockTable() }
}; private RenderFragment RenderStockFromInventoryForm() => __builder => {
@(editingStock == null ? "Add Stock from Inventory" : "Edit Stock Selection")
Lower = used first
@if (!string.IsNullOrEmpty(stockErrorMessage)) {
@stockErrorMessage
}
}; private RenderFragment RenderCustomStockForm() => __builder => {
@(editingStock == null ? "Add Custom Stock Length" : "Edit Custom Stock")
Use -1 for unlimited
Lower = used first
@if (!string.IsNullOrEmpty(stockErrorMessage)) {
@stockErrorMessage
}
}; private RenderFragment RenderStockTable() => __builder => {
@foreach (var stock in job.Stock.OrderBy(s => s.Material?.Shape).ThenBy(s => s.Material?.Size).ThenBy(s => s.Priority)) { }
Material Length Qty Priority Source Actions
@stock.Material?.DisplayName @ArchUnits.FormatFromInches((double)stock.LengthInches) @(stock.Quantity == -1 ? "Unlimited" : stock.Quantity.ToString()) @stock.Priority @if (stock.IsCustomLength) { Custom } else { Inventory }
}; private void ShowAddStockFromInventory() { editingStock = null; newStock = new JobStock { JobId = Id!.Value, Quantity = 1, Priority = 10 }; stockSelectedShape = null; stockSelectedMaterialId = 0; availableStockItems.Clear(); showStockForm = true; showCustomStockForm = false; stockErrorMessage = null; } private void ShowAddCustomStock() { editingStock = null; newStock = new JobStock { JobId = Id!.Value, Quantity = -1, Priority = 10, IsCustomLength = true }; stockSelectedShape = null; showStockForm = false; showCustomStockForm = true; stockErrorMessage = null; } private void CancelStockForm() { showStockForm = false; showCustomStockForm = false; editingStock = null; } private async Task OnStockShapeChanged() { stockSelectedMaterialId = 0; newStock.MaterialId = 0; newStock.StockItemId = null; availableStockItems.Clear(); } private async Task OnStockMaterialChanged() { newStock.MaterialId = stockSelectedMaterialId; newStock.StockItemId = null; if (stockSelectedMaterialId > 0) { availableStockItems = await JobService.GetAvailableStockForMaterialAsync(stockSelectedMaterialId); } else { availableStockItems.Clear(); } } private void EditStock(JobStock stock) { editingStock = stock; newStock = new JobStock { Id = stock.Id, JobId = stock.JobId, MaterialId = stock.MaterialId, StockItemId = stock.StockItemId, LengthInches = stock.LengthInches, Quantity = stock.Quantity, IsCustomLength = stock.IsCustomLength, Priority = stock.Priority, SortOrder = stock.SortOrder }; stockSelectedShape = stock.Material?.Shape; stockSelectedMaterialId = stock.MaterialId; stockErrorMessage = null; if (stock.IsCustomLength) { showStockForm = false; showCustomStockForm = true; } else { showStockForm = true; showCustomStockForm = false; _ = OnStockMaterialChanged(); } } private async Task SaveStockFromInventoryAsync() { stockErrorMessage = null; if (!stockSelectedShape.HasValue) { stockErrorMessage = "Please select a shape"; return; } if (stockSelectedMaterialId == 0) { stockErrorMessage = "Please select a size"; return; } if (!newStock.StockItemId.HasValue) { stockErrorMessage = "Please select a stock length"; return; } if (newStock.Quantity < 1) { stockErrorMessage = "Quantity must be at least 1"; return; } var selectedStock = availableStockItems.FirstOrDefault(s => s.Id == newStock.StockItemId); if (selectedStock == null) { stockErrorMessage = "Selected stock not found"; return; } newStock.MaterialId = stockSelectedMaterialId; newStock.LengthInches = selectedStock.LengthInches; newStock.IsCustomLength = false; if (editingStock == null) { await JobService.AddStockAsync(newStock); } else { await JobService.UpdateStockAsync(newStock); } job = (await JobService.GetByIdAsync(Id!.Value))!; showStockForm = false; editingStock = null; } private async Task SaveCustomStockAsync() { stockErrorMessage = null; if (!stockSelectedShape.HasValue) { stockErrorMessage = "Please select a shape"; return; } if (newStock.MaterialId == 0) { stockErrorMessage = "Please select a size"; return; } if (newStock.LengthInches <= 0) { stockErrorMessage = "Length must be greater than zero"; return; } if (newStock.Quantity < -1 || newStock.Quantity == 0) { stockErrorMessage = "Quantity must be at least 1 (or -1 for unlimited)"; return; } newStock.StockItemId = null; newStock.IsCustomLength = true; if (editingStock == null) { await JobService.AddStockAsync(newStock); } else { await JobService.UpdateStockAsync(newStock); } job = (await JobService.GetByIdAsync(Id!.Value))!; showCustomStockForm = false; editingStock = null; } private async Task DeleteStock(JobStock stock) { await JobService.DeleteStockAsync(stock.Id); job = (await JobService.GetByIdAsync(Id!.Value))!; } }