@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() }
} @* Part Modal Dialog *@ @if (showPartForm) { } @* Import Stock Modal *@ @if (showImportModal) { } @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(); // Import modal private bool showImportModal; private bool loadingImport; private List importCandidates = new(); private string? importErrorMessage; 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.SortOrder).ThenBy(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 (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))!; } // Import modal methods private async Task ShowImportModal() { importErrorMessage = null; importCandidates.Clear(); loadingImport = true; showImportModal = true; try { var materialIds = job.Parts.Select(p => p.MaterialId).Distinct().ToList(); var existingStockItemIds = job.Stock .Where(s => s.StockItemId.HasValue) .Select(s => s.StockItemId!.Value) .ToHashSet(); foreach (var materialId in materialIds) { var stockItems = await JobService.GetAvailableStockForMaterialAsync(materialId); foreach (var item in stockItems.Where(s => !existingStockItemIds.Contains(s.Id))) { importCandidates.Add(new ImportStockCandidate { StockItem = item, Selected = true, Quantity = -1, Priority = 10 }); } } } catch (Exception ex) { importErrorMessage = $"Error loading stock: {ex.Message}"; } finally { loadingImport = false; } } private void CloseImportModal() { showImportModal = false; importCandidates.Clear(); importErrorMessage = null; } private void ToggleAllImportCandidates(bool selected) { foreach (var c in importCandidates) c.Selected = selected; } private async Task ImportSelectedStockAsync() { importErrorMessage = null; var selected = importCandidates.Where(c => c.Selected).ToList(); if (selected.Count == 0) { importErrorMessage = "No items selected"; return; } try { foreach (var candidate in selected) { var jobStock = new JobStock { JobId = Id!.Value, MaterialId = candidate.StockItem.MaterialId, StockItemId = candidate.StockItem.Id, LengthInches = candidate.StockItem.LengthInches, Quantity = candidate.Quantity, Priority = candidate.Priority, IsCustomLength = false }; await JobService.AddStockAsync(jobStock); } job = (await JobService.GetByIdAsync(Id!.Value))!; showImportModal = false; importCandidates.Clear(); } catch (Exception ex) { importErrorMessage = $"Error importing stock: {ex.Message}"; } } private class ImportStockCandidate { public StockItem StockItem { get; set; } = null!; public bool Selected { get; set; } = true; public int Quantity { get; set; } = -1; public int Priority { get; set; } = 10; } }