- Add Stock Items index page listing all stock items - Add Stock Items edit page with supplier offerings management - Update Suppliers edit page to manage offerings (select from stock items instead of material+length) - Add Stock Items navigation link to sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
334 lines
13 KiB
Plaintext
334 lines
13 KiB
Plaintext
@page "/suppliers/new"
|
|
@page "/suppliers/{Id:int}"
|
|
@inject SupplierService SupplierService
|
|
@inject NavigationManager Navigation
|
|
@inject CutList.Web.Data.ApplicationDbContext DbContext
|
|
@using CutList.Core.Formatting
|
|
@using Microsoft.EntityFrameworkCore
|
|
|
|
<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">Supplier Offerings</h5>
|
|
<button class="btn btn-sm btn-primary" @onclick="ShowAddOfferingForm">Add Offering</button>
|
|
</div>
|
|
<div class="card-body">
|
|
@if (showOfferingForm)
|
|
{
|
|
<div class="border rounded p-3 mb-3 bg-light">
|
|
<h6>@(editingOffering == null ? "Add Offering" : "Edit Offering")</h6>
|
|
<div class="row g-2">
|
|
<div class="col-12">
|
|
<label class="form-label">Stock Item</label>
|
|
<select class="form-select" @bind="newOffering.StockItemId">
|
|
<option value="0">-- Select Stock Item --</option>
|
|
@foreach (var stockItem in stockItems)
|
|
{
|
|
<option value="@stockItem.Id">@stockItem.Material.DisplayName - @ArchUnits.FormatFromInches((double)stockItem.LengthInches)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Part Number</label>
|
|
<InputText class="form-control" @bind-Value="newOffering.PartNumber" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Price</label>
|
|
<InputNumber class="form-control" @bind-Value="newOffering.Price" placeholder="0.00" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Supplier Description</label>
|
|
<InputText class="form-control" @bind-Value="newOffering.SupplierDescription" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Notes</label>
|
|
<InputText class="form-control" @bind-Value="newOffering.Notes" />
|
|
</div>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(offeringErrorMessage))
|
|
{
|
|
<div class="alert alert-danger mt-2 mb-0">@offeringErrorMessage</div>
|
|
}
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button class="btn btn-primary btn-sm" @onclick="SaveOfferingAsync" disabled="@savingOffering">
|
|
@if (savingOffering)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
|
}
|
|
@(editingOffering == null ? "Add" : "Save")
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelOfferingForm">Cancel</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (offerings.Count == 0)
|
|
{
|
|
<p class="text-muted">No offerings configured yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Stock Item</th>
|
|
<th>Part #</th>
|
|
<th>Price</th>
|
|
<th style="width: 100px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var offering in offerings)
|
|
{
|
|
<tr>
|
|
<td>@offering.StockItem.Material.DisplayName - @ArchUnits.FormatFromInches((double)offering.StockItem.LengthInches)</td>
|
|
<td>@(offering.PartNumber ?? "-")</td>
|
|
<td>@(offering.Price.HasValue ? offering.Price.Value.ToString("C") : "-")</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditOffering(offering)">Edit</button>
|
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteOffering(offering)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<ConfirmDialog @ref="deleteOfferingDialog"
|
|
Title="Delete Offering"
|
|
Message="@deleteOfferingMessage"
|
|
ConfirmText="Delete"
|
|
OnConfirm="DeleteOfferingConfirmed" />
|
|
|
|
@code {
|
|
[Parameter]
|
|
public int? Id { get; set; }
|
|
|
|
private Supplier supplier = new();
|
|
private List<SupplierOffering> offerings = new();
|
|
private List<StockItem> stockItems = new();
|
|
private bool loading = true;
|
|
private bool savingSupplier;
|
|
private bool savingOffering;
|
|
private string? supplierErrorMessage;
|
|
private string? offeringErrorMessage;
|
|
|
|
private bool showOfferingForm;
|
|
private SupplierOffering newOffering = new();
|
|
private SupplierOffering? editingOffering;
|
|
|
|
private ConfirmDialog deleteOfferingDialog = null!;
|
|
private SupplierOffering? offeringToDelete;
|
|
private string deleteOfferingMessage = "";
|
|
|
|
private bool IsNew => !Id.HasValue;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
stockItems = await DbContext.StockItems
|
|
.Include(si => si.Material)
|
|
.Where(si => si.IsActive)
|
|
.OrderBy(si => si.Material.Shape)
|
|
.ThenBy(si => si.Material.Size)
|
|
.ThenBy(si => si.LengthInches)
|
|
.ToListAsync();
|
|
|
|
if (Id.HasValue)
|
|
{
|
|
var existing = await SupplierService.GetByIdAsync(Id.Value);
|
|
if (existing == null)
|
|
{
|
|
Navigation.NavigateTo("suppliers");
|
|
return;
|
|
}
|
|
supplier = existing;
|
|
offerings = await SupplierService.GetOfferingsForSupplierAsync(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 ShowAddOfferingForm()
|
|
{
|
|
editingOffering = null;
|
|
newOffering = new SupplierOffering { SupplierId = Id!.Value };
|
|
showOfferingForm = true;
|
|
offeringErrorMessage = null;
|
|
}
|
|
|
|
private void EditOffering(SupplierOffering offering)
|
|
{
|
|
editingOffering = offering;
|
|
newOffering = new SupplierOffering
|
|
{
|
|
Id = offering.Id,
|
|
SupplierId = offering.SupplierId,
|
|
StockItemId = offering.StockItemId,
|
|
PartNumber = offering.PartNumber,
|
|
SupplierDescription = offering.SupplierDescription,
|
|
Price = offering.Price,
|
|
Notes = offering.Notes
|
|
};
|
|
showOfferingForm = true;
|
|
offeringErrorMessage = null;
|
|
}
|
|
|
|
private void CancelOfferingForm()
|
|
{
|
|
showOfferingForm = false;
|
|
editingOffering = null;
|
|
offeringErrorMessage = null;
|
|
}
|
|
|
|
private async Task SaveOfferingAsync()
|
|
{
|
|
offeringErrorMessage = null;
|
|
savingOffering = true;
|
|
|
|
try
|
|
{
|
|
if (newOffering.StockItemId == 0)
|
|
{
|
|
offeringErrorMessage = "Please select a stock item";
|
|
return;
|
|
}
|
|
|
|
var exists = await SupplierService.OfferingExistsAsync(
|
|
newOffering.SupplierId,
|
|
newOffering.StockItemId,
|
|
editingOffering?.Id);
|
|
|
|
if (exists)
|
|
{
|
|
offeringErrorMessage = "This supplier already has an offering for this stock item";
|
|
return;
|
|
}
|
|
|
|
if (editingOffering == null)
|
|
{
|
|
await SupplierService.AddOfferingAsync(newOffering);
|
|
}
|
|
else
|
|
{
|
|
await SupplierService.UpdateOfferingAsync(newOffering);
|
|
}
|
|
|
|
offerings = await SupplierService.GetOfferingsForSupplierAsync(Id!.Value);
|
|
showOfferingForm = false;
|
|
editingOffering = null;
|
|
}
|
|
finally
|
|
{
|
|
savingOffering = false;
|
|
}
|
|
}
|
|
|
|
private void ConfirmDeleteOffering(SupplierOffering offering)
|
|
{
|
|
offeringToDelete = offering;
|
|
deleteOfferingMessage = $"Are you sure you want to delete the offering for {offering.StockItem.Material.DisplayName} - {ArchUnits.FormatFromInches((double)offering.StockItem.LengthInches)}?";
|
|
deleteOfferingDialog.Show();
|
|
}
|
|
|
|
private async Task DeleteOfferingConfirmed()
|
|
{
|
|
if (offeringToDelete != null)
|
|
{
|
|
await SupplierService.DeleteOfferingAsync(offeringToDelete.Id);
|
|
offerings = await SupplierService.GetOfferingsForSupplierAsync(Id!.Value);
|
|
}
|
|
}
|
|
}
|