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:
@@ -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,63 +15,170 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-lg-6 mb-4">
|
||||||
<EditForm Model="material" OnValidSubmit="SaveAsync">
|
<div class="card">
|
||||||
<DataAnnotationsValidator />
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Material Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<EditForm Model="material" OnValidSubmit="SaveAsync">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Shape</label>
|
<label class="form-label">Shape</label>
|
||||||
<InputSelect class="form-select" @bind-Value="material.Shape">
|
<InputSelect class="form-select" @bind-Value="material.Shape">
|
||||||
<option value="">-- Select Shape --</option>
|
<option value="">-- Select Shape --</option>
|
||||||
@foreach (var shape in MaterialService.CommonShapes)
|
@foreach (var shape in MaterialService.CommonShapes)
|
||||||
|
{
|
||||||
|
<option value="@shape">@shape</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
<ValidationMessage For="@(() => material.Shape)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<InputText class="form-control" @bind-Value="material.Size" placeholder="e.g., 1" OD x 0.065 wall" />
|
||||||
|
<ValidationMessage For="@(() => material.Size)" />
|
||||||
|
<div class="form-text">Examples: "1" OD x 0.065 wall", "2x2", "1.5 x 1.5 x 0.125"</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description (optional)</label>
|
||||||
|
<InputText class="form-control" @bind-Value="material.Description" placeholder="Optional description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(errorMessage))
|
||||||
{
|
{
|
||||||
<option value="@shape">@shape</option>
|
<div class="alert alert-danger">@errorMessage</div>
|
||||||
}
|
}
|
||||||
</InputSelect>
|
|
||||||
<ValidationMessage For="@(() => material.Shape)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="d-flex gap-2">
|
||||||
<label class="form-label">Size</label>
|
<button type="submit" class="btn btn-primary" disabled="@saving">
|
||||||
<InputText class="form-control" @bind-Value="material.Size" placeholder="e.g., 1" OD x 0.065 wall" />
|
@if (saving)
|
||||||
<ValidationMessage For="@(() => material.Size)" />
|
{
|
||||||
<div class="form-text">Examples: "1" OD x 0.065 wall", "2x2", "1.5 x 1.5 x 0.125"</div>
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
@(IsNew ? "Create Material" : "Save Changes")
|
||||||
|
</button>
|
||||||
|
<a href="materials" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Description (optional)</label>
|
|
||||||
<InputText class="form-control" @bind-Value="material.Description" placeholder="Optional description" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(errorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@errorMessage</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary" disabled="@saving">
|
|
||||||
@if (saving)
|
|
||||||
{
|
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
|
||||||
}
|
|
||||||
@(IsNew ? "Create" : "Save")
|
|
||||||
</button>
|
|
||||||
<a href="materials" class="btn btn-outline-secondary">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</EditForm>
|
|
||||||
</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>
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user