Files
CutList/CutList.Web/Components/Pages/Suppliers/Edit.razor
AJ Isaacs 9868df162d feat: Add CutList.Web Blazor Server application
Add a new web-based frontend for cut list optimization using:
- Blazor Server with .NET 8
- Entity Framework Core with MSSQL LocalDB
- Full CRUD for Materials, Suppliers, Projects, and Cutting Tools
- Supplier stock length management for quick project setup
- Integration with CutList.Core for bin packing optimization
- Print-friendly HTML reports with efficiency statistics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:56:21 -05:00

329 lines
12 KiB
Plaintext

@page "/suppliers/new"
@page "/suppliers/{Id:int}"
@inject SupplierService SupplierService
@inject MaterialService MaterialService
@inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "Add Supplier" : "Edit Supplier")</PageTitle>
<h1>@(IsNew ? "Add Supplier" : supplier.Name)</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">Supplier Details</h5>
</div>
<div class="card-body">
<EditForm Model="supplier" OnValidSubmit="SaveSupplierAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">Name</label>
<InputText class="form-control" @bind-Value="supplier.Name" />
<ValidationMessage For="@(() => supplier.Name)" />
</div>
<div class="mb-3">
<label class="form-label">Contact Info</label>
<InputTextArea class="form-control" @bind-Value="supplier.ContactInfo" rows="2" placeholder="Phone, email, address..." />
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<InputTextArea class="form-control" @bind-Value="supplier.Notes" rows="3" />
</div>
@if (!string.IsNullOrEmpty(supplierErrorMessage))
{
<div class="alert alert-danger">@supplierErrorMessage</div>
}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" disabled="@savingSupplier">
@if (savingSupplier)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(IsNew ? "Create Supplier" : "Save Changes")
</button>
<a href="suppliers" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
</div>
</EditForm>
</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">Stock Lengths</h5>
<button class="btn btn-sm btn-primary" @onclick="ShowAddStockForm">Add Stock</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-6">
<label class="form-label">Material</label>
<select class="form-select" @bind="newStock.MaterialId">
<option value="0">-- Select Material --</option>
@foreach (var material in materials)
{
<option value="@material.Id">@material.DisplayName</option>
}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Length</label>
<LengthInput @bind-Value="newStock.LengthInches" />
</div>
<div class="col-md-6">
<label class="form-label">Price (optional)</label>
<InputNumber class="form-control" @bind-Value="newStock.Price" placeholder="0.00" />
</div>
<div class="col-md-6">
<label class="form-label">Notes</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 (stocks.Count == 0)
{
<p class="text-muted">No stock lengths configured yet.</p>
}
else
{
<table class="table table-sm">
<thead>
<tr>
<th>Material</th>
<th>Length</th>
<th>Price</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var stock in stocks)
{
<tr>
<td>@stock.Material.DisplayName</td>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
<td>@(stock.Price.HasValue ? stock.Price.Value.ToString("C") : "-")</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 {
[Parameter]
public int? Id { get; set; }
private Supplier supplier = new();
private List<SupplierStock> stocks = new();
private List<Material> materials = new();
private bool loading = true;
private bool savingSupplier;
private bool savingStock;
private string? supplierErrorMessage;
private string? stockErrorMessage;
private bool showStockForm;
private SupplierStock newStock = new();
private SupplierStock? editingStock;
private ConfirmDialog deleteStockDialog = null!;
private SupplierStock? stockToDelete;
private string deleteStockMessage = "";
private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
if (Id.HasValue)
{
var existing = await SupplierService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("suppliers");
return;
}
supplier = existing;
stocks = await SupplierService.GetStocksForSupplierAsync(Id.Value);
}
loading = false;
}
private async Task SaveSupplierAsync()
{
supplierErrorMessage = null;
savingSupplier = true;
try
{
if (string.IsNullOrWhiteSpace(supplier.Name))
{
supplierErrorMessage = "Name is required";
return;
}
if (IsNew)
{
var created = await SupplierService.CreateAsync(supplier);
Navigation.NavigateTo($"suppliers/{created.Id}");
}
else
{
await SupplierService.UpdateAsync(supplier);
}
}
finally
{
savingSupplier = false;
}
}
private void ShowAddStockForm()
{
editingStock = null;
newStock = new SupplierStock { SupplierId = Id!.Value };
showStockForm = true;
stockErrorMessage = null;
}
private void EditStock(SupplierStock stock)
{
editingStock = stock;
newStock = new SupplierStock
{
Id = stock.Id,
SupplierId = stock.SupplierId,
MaterialId = stock.MaterialId,
LengthInches = stock.LengthInches,
Price = stock.Price,
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.MaterialId == 0)
{
stockErrorMessage = "Please select a material";
return;
}
if (newStock.LengthInches <= 0)
{
stockErrorMessage = "Length must be greater than zero";
return;
}
var exists = await SupplierService.StockExistsAsync(
newStock.SupplierId,
newStock.MaterialId,
newStock.LengthInches,
editingStock?.Id);
if (exists)
{
stockErrorMessage = "This stock length already exists for this material";
return;
}
if (editingStock == null)
{
await SupplierService.AddStockAsync(newStock);
}
else
{
await SupplierService.UpdateStockAsync(newStock);
}
stocks = await SupplierService.GetStocksForSupplierAsync(Id!.Value);
showStockForm = false;
editingStock = null;
}
finally
{
savingStock = false;
}
}
private void ConfirmDeleteStock(SupplierStock 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 SupplierService.DeleteStockAsync(stockToDelete.Id);
stocks = await SupplierService.GetStocksForSupplierAsync(Id!.Value);
}
}
}