feat: Add stock lengths management UI to Material Edit page

Extends the Material Edit page with a side panel to manage available
stock lengths, including quantity tracking and CRUD operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 23:56:06 -05:00
parent cad5ab790a
commit 8b16cbd79f

View File

@@ -2,10 +2,11 @@
@page "/materials/{Id:int}" @page "/materials/{Id:int}"
@inject MaterialService MaterialService @inject MaterialService MaterialService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "Add Material" : "Edit Material")</PageTitle> <PageTitle>@(IsNew ? "Add Material" : "Edit Material")</PageTitle>
<h1>@(IsNew ? "Add Material" : "Edit Material")</h1> <h1>@(IsNew ? "Add Material" : material.DisplayName)</h1>
@if (loading) @if (loading)
{ {
@@ -14,7 +15,12 @@
else else
{ {
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Material Details</h5>
</div>
<div class="card-body">
<EditForm Model="material" OnValidSubmit="SaveAsync"> <EditForm Model="material" OnValidSubmit="SaveAsync">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
@@ -53,24 +59,126 @@ else
{ {
<span class="spinner-border spinner-border-sm me-1"></span> <span class="spinner-border spinner-border-sm me-1"></span>
} }
@(IsNew ? "Create" : "Save") @(IsNew ? "Create Material" : "Save Changes")
</button> </button>
<a href="materials" class="btn btn-outline-secondary">Cancel</a> <a href="materials" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
</div> </div>
</EditForm> </EditForm>
</div> </div>
</div> </div>
</div>
@if (!IsNew)
{
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Available Stock Lengths</h5>
<button class="btn btn-sm btn-primary" @onclick="ShowAddStockForm">Add Length</button>
</div>
<div class="card-body">
@if (showStockForm)
{
<div class="border rounded p-3 mb-3 bg-light">
<h6>@(editingStock == null ? "Add Stock Length" : "Edit Stock Length")</h6>
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Length</label>
<LengthInput @bind-Value="newStock.LengthInches" />
</div>
<div class="col-md-3">
<label class="form-label">Qty in Stock</label>
<input type="number" class="form-control" @bind="newStock.Quantity" min="0" />
</div>
<div class="col-md-5">
<label class="form-label">Notes (optional)</label>
<InputText class="form-control" @bind-Value="newStock.Notes" />
</div>
</div>
@if (!string.IsNullOrEmpty(stockErrorMessage))
{
<div class="alert alert-danger mt-2 mb-0">@stockErrorMessage</div>
}
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" @onclick="SaveStockAsync" disabled="@savingStock">
@if (savingStock)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(editingStock == null ? "Add" : "Save")
</button>
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelStockForm">Cancel</button>
</div>
</div>
}
@if (stockLengths.Count == 0)
{
<p class="text-muted">No stock lengths configured yet.</p>
<p class="text-muted small">Add common stock lengths for this material (e.g., 20', 24') to quickly populate project stock bins.</p>
}
else
{
<table class="table table-sm">
<thead>
<tr>
<th>Length</th>
<th>Qty</th>
<th>Notes</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var stock in stockLengths)
{
<tr>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
<td>@stock.Quantity</td>
<td>@(stock.Notes ?? "-")</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditStock(stock)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteStock(stock)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
}
</div>
} }
<ConfirmDialog @ref="deleteStockDialog"
Title="Delete Stock Length"
Message="@deleteStockMessage"
ConfirmText="Delete"
OnConfirm="DeleteStockConfirmed" />
@code { @code {
[Parameter] [Parameter]
public int? Id { get; set; } public int? Id { get; set; }
private Material material = new(); private Material material = new();
private List<MaterialStockLength> stockLengths = new();
private bool loading = true; private bool loading = true;
private bool saving; private bool saving;
private string? errorMessage; private string? errorMessage;
// Stock form
private bool showStockForm;
private bool savingStock;
private MaterialStockLength newStock = new();
private MaterialStockLength? editingStock;
private string? stockErrorMessage;
// Delete dialog
private ConfirmDialog deleteStockDialog = null!;
private MaterialStockLength? stockToDelete;
private string deleteStockMessage = "";
private bool IsNew => !Id.HasValue; private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -84,6 +192,7 @@ else
return; return;
} }
material = existing; material = existing;
stockLengths = await MaterialService.GetStockLengthsAsync(Id.Value);
} }
loading = false; loading = false;
} }
@@ -116,18 +225,107 @@ else
if (IsNew) if (IsNew)
{ {
await MaterialService.CreateAsync(material); var created = await MaterialService.CreateAsync(material);
Navigation.NavigateTo($"materials/{created.Id}");
} }
else else
{ {
await MaterialService.UpdateAsync(material); await MaterialService.UpdateAsync(material);
} }
Navigation.NavigateTo("materials");
} }
finally finally
{ {
saving = false; saving = false;
} }
} }
// Stock length methods
private void ShowAddStockForm()
{
editingStock = null;
newStock = new MaterialStockLength { MaterialId = Id!.Value };
showStockForm = true;
stockErrorMessage = null;
}
private void EditStock(MaterialStockLength stock)
{
editingStock = stock;
newStock = new MaterialStockLength
{
Id = stock.Id,
MaterialId = stock.MaterialId,
LengthInches = stock.LengthInches,
Quantity = stock.Quantity,
Notes = stock.Notes
};
showStockForm = true;
stockErrorMessage = null;
}
private void CancelStockForm()
{
showStockForm = false;
editingStock = null;
stockErrorMessage = null;
}
private async Task SaveStockAsync()
{
stockErrorMessage = null;
savingStock = true;
try
{
if (newStock.LengthInches <= 0)
{
stockErrorMessage = "Length must be greater than zero";
return;
}
var exists = await MaterialService.StockLengthExistsAsync(
newStock.MaterialId,
newStock.LengthInches,
editingStock?.Id);
if (exists)
{
stockErrorMessage = "This stock length already exists for this material";
return;
}
if (editingStock == null)
{
await MaterialService.AddStockLengthAsync(newStock);
}
else
{
await MaterialService.UpdateStockLengthAsync(newStock);
}
stockLengths = await MaterialService.GetStockLengthsAsync(Id!.Value);
showStockForm = false;
editingStock = null;
}
finally
{
savingStock = false;
}
}
private void ConfirmDeleteStock(MaterialStockLength stock)
{
stockToDelete = stock;
deleteStockMessage = $"Are you sure you want to delete the {ArchUnits.FormatFromInches((double)stock.LengthInches)} stock length?";
deleteStockDialog.Show();
}
private async Task DeleteStockConfirmed()
{
if (stockToDelete != null)
{
await MaterialService.DeleteStockLengthAsync(stockToDelete.Id);
stockLengths = await MaterialService.GetStockLengthsAsync(Id!.Value);
}
}
} }