Navigation: - Rename Projects to Jobs in NavMenu - Add new icon for multi-material boxes Home page: - Update references from Projects to Jobs Materials pages: - Add Type and Grade columns to index - Shape-specific dimension editing with typed inputs - Error handling with detailed messages Stock pages: - Show Shape, Type, Grade, Size columns - Display QuantityOnHand with badges Shared components: - LengthInput: Add nullable binding mode for optional dimensions - LengthInput: Format on blur for better UX - CutListReport: Update for Job model references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
411 lines
16 KiB
Plaintext
411 lines
16 KiB
Plaintext
@page "/materials/new"
|
|
@page "/materials/{Id:int}"
|
|
@inject MaterialService MaterialService
|
|
@inject NavigationManager Navigation
|
|
@using CutList.Core.Formatting
|
|
@using CutList.Web.Data.Entities
|
|
@using CutList.Web.Components.Shared
|
|
|
|
<PageTitle>@(IsNew ? "Add Material" : "Edit Material")</PageTitle>
|
|
|
|
<h1>@(IsNew ? "Add Material" : material.DisplayName)</h1>
|
|
|
|
@if (loading)
|
|
{
|
|
<p><em>Loading...</em></p>
|
|
}
|
|
else
|
|
{
|
|
<div class="row">
|
|
<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">
|
|
<DataAnnotationsValidator />
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Shape</label>
|
|
<InputSelect class="form-select" @bind-Value="selectedShape" @bind-Value:after="OnShapeChanged">
|
|
<option value="">-- Select Shape --</option>
|
|
@foreach (var shape in Enum.GetValues<MaterialShape>())
|
|
{
|
|
<option value="@shape">@shape.GetDisplayName()</option>
|
|
}
|
|
</InputSelect>
|
|
<ValidationMessage For="@(() => material.Shape)" />
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Type</label>
|
|
<InputSelect class="form-select" @bind-Value="material.Type">
|
|
@foreach (var type in Enum.GetValues<MaterialType>())
|
|
{
|
|
<option value="@type">@type</option>
|
|
}
|
|
</InputSelect>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Grade</label>
|
|
<InputText class="form-control" @bind-Value="material.Grade" placeholder="e.g., A36, Hot Roll, 304" />
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedShape != null)
|
|
{
|
|
@RenderDimensionInputs()
|
|
}
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Size Display (auto-generated)</label>
|
|
<InputText class="form-control" @bind-Value="material.Size" placeholder="Will be auto-generated from dimensions" />
|
|
<div class="form-text">Leave blank to auto-generate from dimensions, or customize as needed.</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 Material" : "Save Changes")
|
|
</button>
|
|
<a href="materials" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
|
|
</div>
|
|
</EditForm>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedShape != null)
|
|
{
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Preview</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<dl class="row mb-0">
|
|
<dt class="col-sm-4">Shape</dt>
|
|
<dd class="col-sm-8">@selectedShape.Value.GetDisplayName()</dd>
|
|
|
|
<dt class="col-sm-4">Type</dt>
|
|
<dd class="col-sm-8">@material.Type</dd>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(material.Grade))
|
|
{
|
|
<dt class="col-sm-4">Grade</dt>
|
|
<dd class="col-sm-8">@material.Grade</dd>
|
|
}
|
|
|
|
<dt class="col-sm-4">Size</dt>
|
|
<dd class="col-sm-8">@GetPreviewSize()</dd>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(material.Description))
|
|
{
|
|
<dt class="col-sm-4">Description</dt>
|
|
<dd class="col-sm-8">@material.Description</dd>
|
|
}
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[Parameter]
|
|
public int? Id { get; set; }
|
|
|
|
private Material material = new();
|
|
private MaterialShape? selectedShape;
|
|
private bool loading = true;
|
|
private bool saving;
|
|
private string? errorMessage;
|
|
|
|
// Typed dimension objects for each shape
|
|
private RoundBarDimensions roundBarDims = new();
|
|
private RoundTubeDimensions roundTubeDims = new();
|
|
private FlatBarDimensions flatBarDims = new();
|
|
private SquareBarDimensions squareBarDims = new();
|
|
private SquareTubeDimensions squareTubeDims = new();
|
|
private RectangularTubeDimensions rectTubeDims = new();
|
|
private AngleDimensions angleDims = new();
|
|
private ChannelDimensions channelDims = new();
|
|
private IBeamDimensions ibeamDims = new();
|
|
private PipeDimensions pipeDims = new();
|
|
|
|
private bool IsNew => !Id.HasValue;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
if (Id.HasValue)
|
|
{
|
|
var existing = await MaterialService.GetByIdAsync(Id.Value);
|
|
if (existing == null)
|
|
{
|
|
Navigation.NavigateTo("materials");
|
|
return;
|
|
}
|
|
material = existing;
|
|
selectedShape = existing.Shape;
|
|
LoadDimensionsFromMaterial(existing);
|
|
}
|
|
loading = false;
|
|
}
|
|
|
|
private void LoadDimensionsFromMaterial(Material m)
|
|
{
|
|
if (m.Dimensions == null) return;
|
|
|
|
switch (m.Dimensions)
|
|
{
|
|
case RoundBarDimensions d: roundBarDims = d; break;
|
|
case RoundTubeDimensions d: roundTubeDims = d; break;
|
|
case FlatBarDimensions d: flatBarDims = d; break;
|
|
case SquareBarDimensions d: squareBarDims = d; break;
|
|
case SquareTubeDimensions d: squareTubeDims = d; break;
|
|
case RectangularTubeDimensions d: rectTubeDims = d; break;
|
|
case AngleDimensions d: angleDims = d; break;
|
|
case ChannelDimensions d: channelDims = d; break;
|
|
case IBeamDimensions d: ibeamDims = d; break;
|
|
case PipeDimensions d: pipeDims = d; break;
|
|
}
|
|
}
|
|
|
|
private MaterialDimensions GetCurrentDimensions() => selectedShape switch
|
|
{
|
|
MaterialShape.RoundBar => roundBarDims,
|
|
MaterialShape.RoundTube => roundTubeDims,
|
|
MaterialShape.FlatBar => flatBarDims,
|
|
MaterialShape.SquareBar => squareBarDims,
|
|
MaterialShape.SquareTube => squareTubeDims,
|
|
MaterialShape.RectangularTube => rectTubeDims,
|
|
MaterialShape.Angle => angleDims,
|
|
MaterialShape.Channel => channelDims,
|
|
MaterialShape.IBeam => ibeamDims,
|
|
MaterialShape.Pipe => pipeDims,
|
|
_ => throw new InvalidOperationException("No shape selected")
|
|
};
|
|
|
|
private void OnShapeChanged()
|
|
{
|
|
if (selectedShape.HasValue)
|
|
{
|
|
material.Shape = selectedShape.Value;
|
|
}
|
|
}
|
|
|
|
private string GetPreviewSize()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(material.Size))
|
|
{
|
|
return material.Size;
|
|
}
|
|
|
|
if (selectedShape.HasValue)
|
|
{
|
|
try
|
|
{
|
|
var generated = GetCurrentDimensions().GenerateSizeString();
|
|
return string.IsNullOrWhiteSpace(generated) ? "(enter dimensions)" : generated;
|
|
}
|
|
catch
|
|
{
|
|
return "(enter dimensions)";
|
|
}
|
|
}
|
|
|
|
return "(select shape and enter dimensions)";
|
|
}
|
|
|
|
private RenderFragment RenderDimensionInputs() => __builder =>
|
|
{
|
|
switch (selectedShape!.Value)
|
|
{
|
|
case MaterialShape.RoundBar:
|
|
<div class="mb-3">
|
|
<label class="form-label">Diameter</label>
|
|
<LengthInput @bind-Value="roundBarDims.Diameter" Placeholder="e.g., 1/2" or 0.5" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.RoundTube:
|
|
<div class="mb-3">
|
|
<label class="form-label">Outer Diameter</label>
|
|
<LengthInput @bind-Value="roundTubeDims.OuterDiameter" Placeholder="e.g., 1" or 1.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Wall Thickness</label>
|
|
<LengthInput @bind-Value="roundTubeDims.Wall" Placeholder="e.g., 0.065 or 1/16"" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.FlatBar:
|
|
<div class="mb-3">
|
|
<label class="form-label">Width</label>
|
|
<LengthInput @bind-Value="flatBarDims.Width" Placeholder="e.g., 2" or 2.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Thickness</label>
|
|
<LengthInput @bind-Value="flatBarDims.Thickness" Placeholder="e.g., 1/4" or 0.25" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.SquareBar:
|
|
<div class="mb-3">
|
|
<label class="form-label">Size</label>
|
|
<LengthInput @bind-Value="squareBarDims.Size" Placeholder="e.g., 1" or 1.0" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.SquareTube:
|
|
<div class="mb-3">
|
|
<label class="form-label">Size</label>
|
|
<LengthInput @bind-Value="squareTubeDims.Size" Placeholder="e.g., 2" or 2.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Wall Thickness</label>
|
|
<LengthInput @bind-Value="squareTubeDims.Wall" Placeholder="e.g., 0.125 or 1/8"" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.RectangularTube:
|
|
<div class="mb-3">
|
|
<label class="form-label">Width</label>
|
|
<LengthInput @bind-Value="rectTubeDims.Width" Placeholder="e.g., 2" or 2.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Height</label>
|
|
<LengthInput @bind-Value="rectTubeDims.Height" Placeholder="e.g., 3" or 3.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Wall Thickness</label>
|
|
<LengthInput @bind-Value="rectTubeDims.Wall" Placeholder="e.g., 0.125 or 1/8"" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.Angle:
|
|
<div class="mb-3">
|
|
<label class="form-label">Leg 1</label>
|
|
<LengthInput @bind-Value="angleDims.Leg1" Placeholder="e.g., 2" or 2.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Leg 2</label>
|
|
<LengthInput @bind-Value="angleDims.Leg2" Placeholder="e.g., 2" or 2.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Thickness</label>
|
|
<LengthInput @bind-Value="angleDims.Thickness" Placeholder="e.g., 1/4" or 0.25" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.Channel:
|
|
<div class="mb-3">
|
|
<label class="form-label">Height</label>
|
|
<LengthInput @bind-Value="channelDims.Height" Placeholder="e.g., 4" or 4.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Flange Width</label>
|
|
<LengthInput @bind-Value="channelDims.Flange" Placeholder="e.g., 1.58" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Web Thickness</label>
|
|
<LengthInput @bind-Value="channelDims.Web" Placeholder="e.g., 0.18" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.IBeam:
|
|
<div class="mb-3">
|
|
<label class="form-label">Height (nominal)</label>
|
|
<LengthInput @bind-Value="ibeamDims.Height" Placeholder="e.g., 8" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Weight per Foot (lbs)</label>
|
|
<input type="number" class="form-control" step="0.01" @bind="ibeamDims.WeightPerFoot" placeholder="e.g., 31" />
|
|
</div>
|
|
break;
|
|
|
|
case MaterialShape.Pipe:
|
|
<div class="mb-3">
|
|
<label class="form-label">Nominal Pipe Size (NPS)</label>
|
|
<LengthInput @bind-Value="pipeDims.NominalSize" Placeholder="e.g., 1" or 1.0" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Schedule (optional)</label>
|
|
<InputText class="form-control" @bind-Value="pipeDims.Schedule" placeholder="e.g., 40, 80, STD" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Wall Thickness (if no schedule)</label>
|
|
<LengthInput @bind-NullableValue="pipeDims.Wall" Placeholder="e.g., 0.133" />
|
|
</div>
|
|
break;
|
|
}
|
|
};
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
errorMessage = null;
|
|
saving = true;
|
|
|
|
try
|
|
{
|
|
if (!selectedShape.HasValue)
|
|
{
|
|
errorMessage = "Shape is required";
|
|
return;
|
|
}
|
|
|
|
material.Shape = selectedShape.Value;
|
|
var dimensions = GetCurrentDimensions();
|
|
|
|
// Auto-generate Size if empty
|
|
if (string.IsNullOrWhiteSpace(material.Size))
|
|
{
|
|
material.Size = dimensions.GenerateSizeString();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(material.Size))
|
|
{
|
|
errorMessage = "Size is required. Please enter dimensions or provide a size string.";
|
|
return;
|
|
}
|
|
|
|
// Check for duplicates
|
|
if (await MaterialService.ExistsAsync(material.Shape, material.Size, Id))
|
|
{
|
|
errorMessage = "A material with this shape and size already exists";
|
|
return;
|
|
}
|
|
|
|
if (IsNew)
|
|
{
|
|
var created = await MaterialService.CreateWithDimensionsAsync(material, dimensions);
|
|
Navigation.NavigateTo($"materials/{created.Id}");
|
|
}
|
|
else
|
|
{
|
|
await MaterialService.UpdateWithDimensionsAsync(material, dimensions);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
saving = false;
|
|
}
|
|
}
|
|
}
|