feat: Update UI for Jobs and enhanced Materials
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>
This commit is contained in:
@@ -14,8 +14,8 @@
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="projects">
|
||||
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Projects
|
||||
<NavLink class="nav-link" href="jobs">
|
||||
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Jobs
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-box' viewBox='0 0 16 16'%3E%3Cpath d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-boxes-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-boxes' viewBox='0 0 16 16'%3E%3Cpath d='M7.752.066a.5.5 0 0 1 .496 0l3.75 2.143a.5.5 0 0 1 .252.434v3.995l3.498 2A.5.5 0 0 1 16 9.07v4.286a.5.5 0 0 1-.252.434l-3.75 2.143a.5.5 0 0 1-.496 0l-3.502-2-3.502 2.001a.5.5 0 0 1-.496 0l-3.75-2.143A.5.5 0 0 1 0 13.357V9.071a.5.5 0 0 1 .252-.434L3.75 6.638V2.643a.5.5 0 0 1 .252-.434zM4.25 7.504 1.508 9.071l2.742 1.567 2.742-1.567zM7.5 9.933l-2.75 1.571v3.134l2.75-1.571zm1 3.134 2.75 1.571v-3.134L8.5 9.933zm.508-3.996 2.742 1.567 2.742-1.567-2.742-1.567zm2.242-2.433V3.504L8.5 5.076V8.21zM7.5 8.21V5.076L4.75 3.504v3.134zM5.258 2.643 8 4.21l2.742-1.567L8 1.076zM15 9.933l-2.75 1.571v3.134L15 13.067zM3.75 14.638v-3.134L1 9.933v3.134z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-building-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-building' viewBox='0 0 16 16'%3E%3Cpath d='M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Z'/%3E%3Cpath d='M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1Zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3V1Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Projects</h5>
|
||||
<p class="card-text">Create and manage cut list projects. Add parts and stock bins, then optimize to minimize waste.</p>
|
||||
<a href="projects" class="btn btn-primary">Go to Projects</a>
|
||||
<h5 class="card-title">Jobs</h5>
|
||||
<p class="card-text">Create and manage cut list jobs. Add parts and stock bins, then optimize to minimize waste.</p>
|
||||
<a href="jobs" class="btn btn-primary">Go to Jobs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
<ol>
|
||||
<li><strong>Set up materials</strong> - Define the shapes and sizes of materials you work with</li>
|
||||
<li><strong>Add suppliers</strong> - Track which stock lengths are available from your suppliers</li>
|
||||
<li><strong>Create a project</strong> - Add the parts you need to cut with their lengths and quantities</li>
|
||||
<li><strong>Create a job</strong> - Add the parts you need to cut with their lengths and quantities</li>
|
||||
<li><strong>Add stock bins</strong> - Specify which stock lengths to cut from (import from supplier or add manually)</li>
|
||||
<li><strong>Optimize</strong> - Run the optimizer to find the best cutting pattern</li>
|
||||
<li><strong>Print report</strong> - Generate a printable cut list to take to the shop</li>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@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>
|
||||
|
||||
@@ -26,21 +28,41 @@ else
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Shape</label>
|
||||
<InputSelect class="form-select" @bind-Value="material.Shape">
|
||||
<InputSelect class="form-select" @bind-Value="selectedShape" @bind-Value:after="OnShapeChanged">
|
||||
<option value="">-- Select Shape --</option>
|
||||
@foreach (var shape in MaterialService.CommonShapes)
|
||||
@foreach (var shape in Enum.GetValues<MaterialShape>())
|
||||
{
|
||||
<option value="@shape">@shape</option>
|
||||
<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</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>
|
||||
<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">
|
||||
@@ -68,82 +90,36 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!IsNew)
|
||||
@if (selectedShape != null)
|
||||
{
|
||||
<div class="col-lg-6">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<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 class="card-header">
|
||||
<h5 class="mb-0">Preview</h5>
|
||||
</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>
|
||||
}
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Shape</dt>
|
||||
<dd class="col-sm-8">@selectedShape.Value.GetDisplayName()</dd>
|
||||
|
||||
@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>
|
||||
}
|
||||
<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>
|
||||
@@ -151,33 +127,27 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
<ConfirmDialog @ref="deleteStockDialog"
|
||||
Title="Delete Stock Length"
|
||||
Message="@deleteStockMessage"
|
||||
ConfirmText="Delete"
|
||||
OnConfirm="DeleteStockConfirmed" />
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int? Id { get; set; }
|
||||
|
||||
private Material material = new();
|
||||
private List<MaterialStockLength> stockLengths = new();
|
||||
private MaterialShape? selectedShape;
|
||||
private bool loading = true;
|
||||
private bool saving;
|
||||
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 = "";
|
||||
// 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;
|
||||
|
||||
@@ -192,11 +162,201 @@ else
|
||||
return;
|
||||
}
|
||||
material = existing;
|
||||
stockLengths = await MaterialService.GetStockLengthsAsync(Id.Value);
|
||||
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;
|
||||
@@ -204,15 +364,24 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(material.Shape))
|
||||
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))
|
||||
{
|
||||
errorMessage = "Size is required";
|
||||
material.Size = dimensions.GenerateSizeString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(material.Size))
|
||||
{
|
||||
errorMessage = "Size is required. Please enter dimensions or provide a size string.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,12 +394,12 @@ else
|
||||
|
||||
if (IsNew)
|
||||
{
|
||||
var created = await MaterialService.CreateAsync(material);
|
||||
var created = await MaterialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
Navigation.NavigateTo($"materials/{created.Id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await MaterialService.UpdateAsync(material);
|
||||
await MaterialService.UpdateWithDimensionsAsync(material, dimensions);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -238,94 +407,4 @@ else
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error loading materials:</strong>
|
||||
<pre style="white-space: pre-wrap;">@errorMessage</pre>
|
||||
</div>
|
||||
}
|
||||
else if (materials.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
@@ -25,21 +32,27 @@ else
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Shape</th>
|
||||
<th>Type</th>
|
||||
<th>Grade</th>
|
||||
<th>Size</th>
|
||||
<th>Description</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
<th style="width: 160px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var material in materials)
|
||||
{
|
||||
<tr>
|
||||
<td>@material.Shape</td>
|
||||
<td>@material.Shape.GetDisplayName()</td>
|
||||
<td>@material.Type</td>
|
||||
<td>@material.Grade</td>
|
||||
<td>@material.Size</td>
|
||||
<td>@material.Description</td>
|
||||
<td>
|
||||
<a href="materials/@material.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(material)">Delete</button>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="materials/@material.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(material)">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -56,14 +69,25 @@ else
|
||||
@code {
|
||||
private List<Material> materials = new();
|
||||
private bool loading = true;
|
||||
private string? errorMessage;
|
||||
private ConfirmDialog deleteDialog = null!;
|
||||
private Material? materialToDelete;
|
||||
private string deleteMessage = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
materials = await MaterialService.GetAllAsync();
|
||||
loading = false;
|
||||
try
|
||||
{
|
||||
materials = await MaterialService.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfirmDelete(Material material)
|
||||
|
||||
@@ -54,6 +54,11 @@ else
|
||||
<InputText class="form-control" @bind-Value="stockItem.Name" placeholder="Custom display name" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Notes (optional)</label>
|
||||
<InputText class="form-control" @bind-Value="stockItem.Notes" placeholder="Internal notes" />
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger">@errorMessage</div>
|
||||
@@ -76,7 +81,111 @@ else
|
||||
|
||||
@if (!IsNew)
|
||||
{
|
||||
<div class="col-lg-6">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
Inventory
|
||||
<span class="badge @(stockItem.QuantityOnHand > 0 ? "bg-success" : "bg-secondary") ms-2">@stockItem.QuantityOnHand on hand</span>
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-primary" @onclick="ShowStockForm">Add/Adjust Stock</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (showStockForm)
|
||||
{
|
||||
<div class="border rounded p-3 mb-3 bg-light">
|
||||
<h6>Stock Transaction</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select" @bind="stockTransactionType">
|
||||
<option value="add">Receive Stock</option>
|
||||
<option value="adjust">Set Quantity</option>
|
||||
<option value="scrap">Scrap/Waste</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">@(stockTransactionType == "adjust" ? "New Quantity" : "Quantity")</label>
|
||||
<input type="number" class="form-control" @bind="stockQuantity" min="0" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Notes</label>
|
||||
<InputText class="form-control" @bind-Value="stockNotes" />
|
||||
</div>
|
||||
@if (stockTransactionType == "add")
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Supplier (optional)</label>
|
||||
<select class="form-select" @bind="stockSupplierId">
|
||||
<option value="0">-- Select Supplier --</option>
|
||||
@foreach (var supplier in suppliers)
|
||||
{
|
||||
<option value="@supplier.Id">@supplier.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Unit Price (optional)</label>
|
||||
<input type="number" class="form-control" @bind="stockUnitPrice" step="0.01" min="0" placeholder="0.00" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(stockFormErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mt-2 mb-0">@stockFormErrorMessage</div>
|
||||
}
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-primary btn-sm" @onclick="SaveStockTransactionAsync" disabled="@savingStockTransaction">
|
||||
@if (savingStockTransaction)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
}
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelStockForm">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (transactions.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No transaction history yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Qty</th>
|
||||
<th>Supplier</th>
|
||||
<th>Price</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var txn in transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@txn.CreatedAt.ToLocalTime().ToString("MM/dd/yy HH:mm")</td>
|
||||
<td>
|
||||
<span class="badge @GetTransactionBadgeClass(txn.Type)">@txn.Type</span>
|
||||
</td>
|
||||
<td class="@(txn.Quantity >= 0 ? "text-success" : "text-danger")">
|
||||
@(txn.Quantity >= 0 ? "+" : "")@txn.Quantity
|
||||
</td>
|
||||
<td>@(txn.Supplier?.Name ?? "-")</td>
|
||||
<td>@(txn.UnitPrice.HasValue ? txn.UnitPrice.Value.ToString("C") : "-")</td>
|
||||
<td>@(txn.Notes ?? "-")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Supplier Offerings</h5>
|
||||
@@ -184,6 +293,7 @@ else
|
||||
private List<Material> materials = new();
|
||||
private List<Supplier> suppliers = new();
|
||||
private List<SupplierOffering> offerings = new();
|
||||
private List<StockTransaction> transactions = new();
|
||||
private bool loading = true;
|
||||
private bool saving;
|
||||
private bool savingOffering;
|
||||
@@ -194,6 +304,16 @@ else
|
||||
private SupplierOffering newOffering = new();
|
||||
private SupplierOffering? editingOffering;
|
||||
|
||||
// Stock transaction form
|
||||
private bool showStockForm;
|
||||
private bool savingStockTransaction;
|
||||
private string stockTransactionType = "add";
|
||||
private int stockQuantity;
|
||||
private int stockSupplierId;
|
||||
private decimal? stockUnitPrice;
|
||||
private string? stockNotes;
|
||||
private string? stockFormErrorMessage;
|
||||
|
||||
private ConfirmDialog deleteOfferingDialog = null!;
|
||||
private SupplierOffering? offeringToDelete;
|
||||
private string deleteOfferingMessage = "";
|
||||
@@ -215,10 +335,90 @@ else
|
||||
}
|
||||
stockItem = existing;
|
||||
offerings = existing.SupplierOfferings.Where(o => o.IsActive).ToList();
|
||||
transactions = await StockItemService.GetTransactionHistoryAsync(Id.Value, 20);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private string GetTransactionBadgeClass(StockTransactionType type) => type switch
|
||||
{
|
||||
StockTransactionType.Received => "bg-success",
|
||||
StockTransactionType.Used => "bg-primary",
|
||||
StockTransactionType.Adjustment => "bg-warning text-dark",
|
||||
StockTransactionType.Scrapped => "bg-danger",
|
||||
StockTransactionType.Returned => "bg-info",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
|
||||
private void ShowStockForm()
|
||||
{
|
||||
stockTransactionType = "add";
|
||||
stockQuantity = 0;
|
||||
stockSupplierId = 0;
|
||||
stockUnitPrice = null;
|
||||
stockNotes = null;
|
||||
stockFormErrorMessage = null;
|
||||
showStockForm = true;
|
||||
}
|
||||
|
||||
private void CancelStockForm()
|
||||
{
|
||||
showStockForm = false;
|
||||
stockFormErrorMessage = null;
|
||||
}
|
||||
|
||||
private async Task SaveStockTransactionAsync()
|
||||
{
|
||||
stockFormErrorMessage = null;
|
||||
savingStockTransaction = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (stockQuantity <= 0 && stockTransactionType != "adjust")
|
||||
{
|
||||
stockFormErrorMessage = "Quantity must be greater than zero";
|
||||
return;
|
||||
}
|
||||
|
||||
if (stockTransactionType == "adjust" && stockQuantity < 0)
|
||||
{
|
||||
stockFormErrorMessage = "Quantity cannot be negative";
|
||||
return;
|
||||
}
|
||||
|
||||
switch (stockTransactionType)
|
||||
{
|
||||
case "add":
|
||||
await StockItemService.AddStockAsync(
|
||||
Id!.Value,
|
||||
stockQuantity,
|
||||
stockSupplierId > 0 ? stockSupplierId : null,
|
||||
stockUnitPrice,
|
||||
stockNotes);
|
||||
break;
|
||||
case "adjust":
|
||||
await StockItemService.AdjustStockAsync(Id!.Value, stockQuantity, stockNotes);
|
||||
break;
|
||||
case "scrap":
|
||||
await StockItemService.ScrapStockAsync(Id!.Value, stockQuantity, stockNotes);
|
||||
break;
|
||||
}
|
||||
|
||||
// Refresh
|
||||
var updated = await StockItemService.GetByIdAsync(Id!.Value);
|
||||
if (updated != null)
|
||||
{
|
||||
stockItem = updated;
|
||||
}
|
||||
transactions = await StockItemService.GetTransactionHistoryAsync(Id!.Value, 20);
|
||||
showStockForm = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
savingStockTransaction = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveStockItemAsync()
|
||||
{
|
||||
errorMessage = null;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@inject StockItemService StockItemService
|
||||
@inject NavigationManager Navigation
|
||||
@using CutList.Core.Formatting
|
||||
@using CutList.Web.Data.Entities
|
||||
|
||||
<PageTitle>Stock Items</PageTitle>
|
||||
|
||||
@@ -25,22 +26,39 @@ else
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Material</th>
|
||||
<th>Shape</th>
|
||||
<th>Type</th>
|
||||
<th>Grade</th>
|
||||
<th>Size</th>
|
||||
<th>Length</th>
|
||||
<th>Name</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
<th>On Hand</th>
|
||||
<th style="width: 160px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in stockItems)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Material.DisplayName</td>
|
||||
<td>@item.Material.Shape.GetDisplayName()</td>
|
||||
<td>@item.Material.Type</td>
|
||||
<td>@item.Material.Grade</td>
|
||||
<td>@item.Material.Size</td>
|
||||
<td>@ArchUnits.FormatFromInches((double)item.LengthInches)</td>
|
||||
<td>@(item.Name ?? "-")</td>
|
||||
<td>
|
||||
<a href="stock/@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(item)">Delete</button>
|
||||
@if (item.QuantityOnHand > 0)
|
||||
{
|
||||
<span class="badge bg-success">@item.QuantityOnHand</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">0</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="stock/@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(item)">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ else
|
||||
<th>Name</th>
|
||||
<th>Contact Info</th>
|
||||
<th>Notes</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
<th style="width: 160px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -38,8 +38,10 @@ else
|
||||
<td>@supplier.ContactInfo</td>
|
||||
<td>@TruncateText(supplier.Notes, 50)</td>
|
||||
<td>
|
||||
<a href="suppliers/@supplier.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(supplier)">Delete</button>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="suppliers/@supplier.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(supplier)">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/tools"
|
||||
@inject ProjectService ProjectService
|
||||
@inject JobService JobService
|
||||
@using CutList.Core.Formatting
|
||||
|
||||
<PageTitle>Cutting Tools</PageTitle>
|
||||
@@ -122,7 +122,7 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
tools = await ProjectService.GetCuttingToolsAsync();
|
||||
tools = await JobService.GetCuttingToolsAsync();
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -177,14 +177,14 @@ else
|
||||
|
||||
if (editingTool == null)
|
||||
{
|
||||
await ProjectService.CreateCuttingToolAsync(formTool);
|
||||
await JobService.CreateCuttingToolAsync(formTool);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProjectService.UpdateCuttingToolAsync(formTool);
|
||||
await JobService.UpdateCuttingToolAsync(formTool);
|
||||
}
|
||||
|
||||
tools = await ProjectService.GetCuttingToolsAsync();
|
||||
tools = await JobService.GetCuttingToolsAsync();
|
||||
showForm = false;
|
||||
editingTool = null;
|
||||
}
|
||||
@@ -205,8 +205,8 @@ else
|
||||
{
|
||||
if (toolToDelete != null)
|
||||
{
|
||||
await ProjectService.DeleteCuttingToolAsync(toolToDelete.Id);
|
||||
tools = await ProjectService.GetCuttingToolsAsync();
|
||||
await JobService.DeleteCuttingToolAsync(toolToDelete.Id);
|
||||
tools = await JobService.GetCuttingToolsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<h1>CUT LIST</h1>
|
||||
<div class="meta-info">
|
||||
<div class="meta-row"><span>Date:</span> @DateTime.Now.ToString("g")</div>
|
||||
<div class="meta-row"><span>Project:</span> @Project.Name</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Project.Customer))
|
||||
<div class="meta-row"><span>Job:</span> @Job.Name</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Job.Customer))
|
||||
{
|
||||
<div class="meta-row"><span>Customer:</span> @Project.Customer</div>
|
||||
<div class="meta-row"><span>Customer:</span> @Job.Customer</div>
|
||||
}
|
||||
@if (Project.CuttingTool != null)
|
||||
@if (Job.CuttingTool != null)
|
||||
{
|
||||
<div class="meta-row"><span>Cut Method:</span> @Project.CuttingTool.Name (kerf: @Project.CuttingTool.KerfInches")</div>
|
||||
<div class="meta-row"><span>Cut Method:</span> @Job.CuttingTool.Name (kerf: @Job.CuttingTool.KerfInches")</div>
|
||||
}
|
||||
<div class="meta-row"><span>Stock Bars:</span> @PackResult.Bins.Count</div>
|
||||
<div class="meta-row"><span>Total Pieces:</span> @TotalPieces</div>
|
||||
@@ -64,18 +64,18 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Project.Notes))
|
||||
@if (!string.IsNullOrEmpty(Job.Notes))
|
||||
{
|
||||
<div class="notes-section">
|
||||
<h3>Notes</h3>
|
||||
<p>@Project.Notes</p>
|
||||
<p>@Job.Notes</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public Project Project { get; set; } = null!;
|
||||
public Job Job { get; set; } = null!;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public PackResult PackResult { get; set; } = null!;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<input type="text"
|
||||
class="form-control @(HasError ? "is-invalid" : "")"
|
||||
value="@DisplayValue"
|
||||
@onchange="OnInputChange"
|
||||
@oninput="OnInputChange"
|
||||
@onblur="OnBlur"
|
||||
placeholder="@Placeholder" />
|
||||
@if (HasError)
|
||||
{
|
||||
@@ -13,24 +14,53 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Non-nullable decimal value binding.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public decimal Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<decimal> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nullable decimal value binding (used for optional dimension fields).
|
||||
/// Takes precedence over Value if both ValueChanged and NullableValueChanged are set.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public decimal? NullableValue { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<decimal?> NullableValueChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = "e.g., 12' 6\" or 144";
|
||||
|
||||
private string DisplayValue { get; set; } = string.Empty;
|
||||
private bool HasError { get; set; }
|
||||
private string ErrorMessage { get; set; } = string.Empty;
|
||||
private decimal? _lastValue;
|
||||
|
||||
private bool IsNullableMode => NullableValueChanged.HasDelegate;
|
||||
|
||||
private decimal? CurrentValue => IsNullableMode ? NullableValue : Value;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Value > 0 && string.IsNullOrEmpty(DisplayValue))
|
||||
// Reset display when Value changes externally (e.g., form reset)
|
||||
if (CurrentValue != _lastValue)
|
||||
{
|
||||
DisplayValue = ArchUnits.FormatFromInches((double)Value);
|
||||
_lastValue = CurrentValue;
|
||||
if (CurrentValue.HasValue && CurrentValue.Value > 0)
|
||||
{
|
||||
DisplayValue = ArchUnits.FormatFromInches((double)CurrentValue.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayValue = string.Empty;
|
||||
HasError = false;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +73,15 @@
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
await ValueChanged.InvokeAsync(0);
|
||||
_lastValue = null;
|
||||
if (IsNullableMode)
|
||||
{
|
||||
await NullableValueChanged.InvokeAsync(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ValueChanged.InvokeAsync(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,14 +89,32 @@
|
||||
{
|
||||
// Try to parse as architectural units
|
||||
var inches = ArchUnits.ParseToInches(input);
|
||||
await ValueChanged.InvokeAsync((decimal)inches);
|
||||
_lastValue = (decimal)inches;
|
||||
|
||||
if (IsNullableMode)
|
||||
{
|
||||
await NullableValueChanged.InvokeAsync(_lastValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ValueChanged.InvokeAsync(_lastValue.Value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Try to parse as plain decimal (inches)
|
||||
if (decimal.TryParse(input, out var decimalValue))
|
||||
{
|
||||
await ValueChanged.InvokeAsync(decimalValue);
|
||||
_lastValue = decimalValue;
|
||||
|
||||
if (IsNullableMode)
|
||||
{
|
||||
await NullableValueChanged.InvokeAsync(decimalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ValueChanged.InvokeAsync(decimalValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -68,6 +124,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBlur()
|
||||
{
|
||||
// Format the display value nicely on blur if we have a valid value
|
||||
if (!HasError && _lastValue.HasValue && _lastValue.Value > 0)
|
||||
{
|
||||
DisplayValue = ArchUnits.FormatFromInches((double)_lastValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static string FormatLength(decimal inches)
|
||||
{
|
||||
return ArchUnits.FormatFromInches((double)inches);
|
||||
|
||||
Reference in New Issue
Block a user