feat: remove Suppliers and Orders pages and nav entries
This commit is contained in:
@@ -28,16 +28,6 @@
|
||||
<span class="bi bi-boxes-nav-menu" aria-hidden="true"></span> Stock Items
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="orders">
|
||||
<span class="bi bi-cart-nav-menu" aria-hidden="true"></span> Orders
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="suppliers">
|
||||
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Suppliers
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="tools">
|
||||
<span class="bi bi-tools-nav-menu" aria-hidden="true"></span> Cutting Tools
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p class="lead">1D Bin Packing Optimization for Material Cutting</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Jobs</h5>
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Materials</h5>
|
||||
@@ -25,16 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Suppliers</h5>
|
||||
<p class="card-text">Track suppliers and their available stock lengths for quick project setup.</p>
|
||||
<a href="suppliers" class="btn btn-outline-primary">Manage Suppliers</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Cutting Tools</h5>
|
||||
@@ -50,9 +41,9 @@
|
||||
<h4>How It Works</h4>
|
||||
<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>Add stock</strong> - Record which stock lengths you have on hand</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>Add stock bins</strong> - Specify which stock lengths to cut from</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>
|
||||
</ol>
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
@page "/orders/add"
|
||||
@inject PurchaseItemService PurchaseItemService
|
||||
@inject StockItemService StockItemService
|
||||
@inject SupplierService SupplierService
|
||||
@inject JobService JobService
|
||||
@inject NavigationManager Navigation
|
||||
@using CutList.Core.Formatting
|
||||
@using CutList.Web.Data.Entities
|
||||
|
||||
<PageTitle>Add Order Item</PageTitle>
|
||||
|
||||
<h1>Add Order Item</h1>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Order Item Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<EditForm Model="item" OnValidSubmit="SaveAsync">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Stock Item</label>
|
||||
<select class="form-select" @bind="item.StockItemId">
|
||||
<option value="0">-- Select Stock Item --</option>
|
||||
@foreach (var group in stockItemGroups)
|
||||
{
|
||||
<optgroup label="@group.Key">
|
||||
@foreach (var si in group.Value)
|
||||
{
|
||||
<option value="@si.Id">@si.Material.Size - @ArchUnits.FormatFromInches((double)si.LengthInches)</option>
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<InputNumber class="form-control" @bind-Value="item.Quantity" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Supplier (optional)</label>
|
||||
<select class="form-select" @bind="item.SupplierId">
|
||||
<option value="">-- Select Supplier --</option>
|
||||
@foreach (var supplier in suppliers)
|
||||
{
|
||||
<option value="@supplier.Id">@supplier.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Job (optional)</label>
|
||||
<select class="form-select" @bind="item.JobId">
|
||||
<option value="">-- Select Job --</option>
|
||||
@foreach (var job in jobs)
|
||||
{
|
||||
<option value="@job.Id">@job.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Notes (optional)</label>
|
||||
<InputText class="form-control" @bind-Value="item.Notes" placeholder="Any notes about this order" />
|
||||
</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>
|
||||
}
|
||||
Add to Order List
|
||||
</button>
|
||||
<a href="orders" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private PurchaseItem item = new() { Quantity = 1 };
|
||||
private List<StockItem> stockItems = new();
|
||||
private Dictionary<string, List<StockItem>> stockItemGroups = new();
|
||||
private List<Supplier> suppliers = new();
|
||||
private List<Job> jobs = new();
|
||||
private bool loading = true;
|
||||
private bool saving;
|
||||
private string? errorMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
stockItems = await StockItemService.GetAllAsync();
|
||||
suppliers = await SupplierService.GetAllAsync();
|
||||
jobs = await JobService.GetAllAsync();
|
||||
|
||||
stockItemGroups = stockItems
|
||||
.GroupBy(s => s.Material.Shape.GetDisplayName())
|
||||
.OrderBy(g => g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(s => s.Material.Size).ThenBy(s => s.LengthInches).ToList());
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
errorMessage = null;
|
||||
saving = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (item.StockItemId == 0)
|
||||
{
|
||||
errorMessage = "Please select a stock item";
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Quantity <= 0)
|
||||
{
|
||||
errorMessage = "Quantity must be at least 1";
|
||||
return;
|
||||
}
|
||||
|
||||
await PurchaseItemService.CreateAsync(item);
|
||||
Navigation.NavigateTo("orders");
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
@page "/orders"
|
||||
@inject PurchaseItemService PurchaseItemService
|
||||
@inject SupplierService SupplierService
|
||||
@inject NavigationManager Navigation
|
||||
@using CutList.Core.Formatting
|
||||
@using CutList.Web.Data.Entities
|
||||
|
||||
<PageTitle>Orders</PageTitle>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>To Be Ordered</h1>
|
||||
<a href="orders/add" class="btn btn-primary">Add Item</a>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Track material that needs to be ordered from suppliers. Items are added manually or automatically from job optimization results.
|
||||
</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "pending" ? "active" : "")" @onclick='() => SetTab("pending")'>
|
||||
Pending
|
||||
@if (pendingCount > 0)
|
||||
{
|
||||
<span class="badge bg-warning text-dark ms-1">@pendingCount</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "ordered" ? "active" : "")" @onclick='() => SetTab("ordered")'>
|
||||
Ordered
|
||||
@if (orderedCount > 0)
|
||||
{
|
||||
<span class="badge bg-primary ms-1">@orderedCount</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "all" ? "active" : "")" @onclick='() => SetTab("all")'>All</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (tabItems.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
@if (activeTab == "pending")
|
||||
{
|
||||
<span>No pending items. <a href="orders/add">Add an item</a> or use "Add to Order List" from a job's results page.</span>
|
||||
}
|
||||
else if (activeTab == "ordered")
|
||||
{
|
||||
<span>No ordered items.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>No order items found. <a href="orders/add">Add your first item</a>.</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MaterialFilter AvailableGrades="availableGrades" Value="filterState" ValueChanged="OnFilterChanged" />
|
||||
|
||||
@if (filteredItems.Count == 0)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
No items match your filters.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Material</th>
|
||||
<th>Length</th>
|
||||
<th>Qty</th>
|
||||
<th>Supplier</th>
|
||||
<th>Job</th>
|
||||
<th>Status</th>
|
||||
<th>Notes</th>
|
||||
<th style="width: 140px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in pagedItems)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.StockItem.Material.DisplayName</td>
|
||||
<td>@ArchUnits.FormatFromInches((double)item.StockItem.LengthInches)</td>
|
||||
<td>@item.Quantity</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" style="min-width: 140px;"
|
||||
value="@(item.SupplierId?.ToString() ?? "")"
|
||||
@onchange="(e) => OnSupplierChanged(item, e)">
|
||||
<option value="">-- Select --</option>
|
||||
@foreach (var supplier in suppliers)
|
||||
{
|
||||
<option value="@supplier.Id">@supplier.Name</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
@if (item.Job != null)
|
||||
{
|
||||
<a href="jobs/@item.Job.Id">@item.Job.DisplayName</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @GetStatusBadgeClass(item.Status)">@item.Status</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">@(item.Notes ?? "-")</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
@if (item.Status == PurchaseItemStatus.Pending)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => MarkOrdered(item)" title="Mark Ordered">
|
||||
<i class="bi bi-truck"></i>
|
||||
</button>
|
||||
}
|
||||
@if (item.Status == PurchaseItemStatus.Ordered)
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-success" @onclick="() => MarkReceived(item)" title="Mark Received">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(item)" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pager TotalCount="filteredItems.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<ConfirmDialog @ref="deleteDialog"
|
||||
Title="Delete Order Item"
|
||||
Message="@deleteMessage"
|
||||
ConfirmText="Delete"
|
||||
OnConfirm="DeleteConfirmed" />
|
||||
|
||||
@code {
|
||||
private List<PurchaseItem> allItems = new();
|
||||
private List<Supplier> suppliers = new();
|
||||
private bool loading = true;
|
||||
private string activeTab = "pending";
|
||||
private int currentPage = 1;
|
||||
private int pageSize = 25;
|
||||
private ConfirmDialog deleteDialog = null!;
|
||||
private PurchaseItem? itemToDelete;
|
||||
private string deleteMessage = "";
|
||||
private MaterialFilterState filterState = new();
|
||||
|
||||
private int pendingCount => allItems.Count(i => i.Status == PurchaseItemStatus.Pending);
|
||||
private int orderedCount => allItems.Count(i => i.Status == PurchaseItemStatus.Ordered);
|
||||
|
||||
private List<PurchaseItem> tabItems => activeTab switch
|
||||
{
|
||||
"pending" => allItems.Where(i => i.Status == PurchaseItemStatus.Pending).ToList(),
|
||||
"ordered" => allItems.Where(i => i.Status == PurchaseItemStatus.Ordered).ToList(),
|
||||
_ => allItems
|
||||
};
|
||||
|
||||
private List<PurchaseItem> filteredItems => tabItems.Where(i =>
|
||||
{
|
||||
var m = i.StockItem.Material;
|
||||
if (filterState.Shape.HasValue && m.Shape != filterState.Shape.Value)
|
||||
return false;
|
||||
if (filterState.Type.HasValue && m.Type != filterState.Type.Value)
|
||||
return false;
|
||||
if (!string.IsNullOrEmpty(filterState.Grade) && m.Grade != filterState.Grade)
|
||||
return false;
|
||||
if (!string.IsNullOrWhiteSpace(filterState.SearchText))
|
||||
{
|
||||
var search = filterState.SearchText.Trim();
|
||||
if (!Contains(m.Size, search)
|
||||
&& !Contains(m.Grade, search)
|
||||
&& !Contains(m.Shape.GetDisplayName(), search)
|
||||
&& !Contains(i.Notes, search)
|
||||
&& !Contains(i.Job?.DisplayName, search)
|
||||
&& !Contains(i.Supplier?.Name, search))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).ToList();
|
||||
|
||||
private IEnumerable<string> availableGrades => tabItems
|
||||
.Select(i => i.StockItem.Material.Grade)
|
||||
.Where(g => !string.IsNullOrEmpty(g))
|
||||
.Distinct()
|
||||
.OrderBy(g => g)!;
|
||||
|
||||
private IEnumerable<PurchaseItem> pagedItems => filteredItems
|
||||
.Skip((currentPage - 1) * pageSize)
|
||||
.Take(pageSize);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
allItems = await PurchaseItemService.GetAllAsync();
|
||||
suppliers = await SupplierService.GetAllAsync();
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private void SetTab(string tab)
|
||||
{
|
||||
activeTab = tab;
|
||||
currentPage = 1;
|
||||
filterState = new();
|
||||
}
|
||||
|
||||
private void OnFilterChanged(MaterialFilterState state)
|
||||
{
|
||||
filterState = state;
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
private async Task OnSupplierChanged(PurchaseItem item, ChangeEventArgs e)
|
||||
{
|
||||
int? supplierId = int.TryParse(e.Value?.ToString(), out var id) && id > 0 ? id : null;
|
||||
await PurchaseItemService.UpdateSupplierAsync(item.Id, supplierId);
|
||||
item.SupplierId = supplierId;
|
||||
item.Supplier = supplierId.HasValue ? suppliers.FirstOrDefault(s => s.Id == supplierId.Value) : null;
|
||||
}
|
||||
|
||||
private async Task MarkOrdered(PurchaseItem item)
|
||||
{
|
||||
await PurchaseItemService.UpdateStatusAsync(item.Id, PurchaseItemStatus.Ordered);
|
||||
item.Status = PurchaseItemStatus.Ordered;
|
||||
}
|
||||
|
||||
private async Task MarkReceived(PurchaseItem item)
|
||||
{
|
||||
await PurchaseItemService.UpdateStatusAsync(item.Id, PurchaseItemStatus.Received);
|
||||
allItems = await PurchaseItemService.GetAllAsync();
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)filteredItems.Count / pageSize);
|
||||
if (currentPage > totalPages && totalPages > 0)
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
private void ConfirmDelete(PurchaseItem item)
|
||||
{
|
||||
itemToDelete = item;
|
||||
deleteMessage = $"Are you sure you want to delete this order item ({item.StockItem.Material.DisplayName} - {ArchUnits.FormatFromInches((double)item.StockItem.LengthInches)} x{item.Quantity})?";
|
||||
deleteDialog.Show();
|
||||
}
|
||||
|
||||
private async Task DeleteConfirmed()
|
||||
{
|
||||
if (itemToDelete != null)
|
||||
{
|
||||
await PurchaseItemService.DeleteAsync(itemToDelete.Id);
|
||||
allItems = await PurchaseItemService.GetAllAsync();
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)filteredItems.Count / pageSize);
|
||||
if (currentPage > totalPages && totalPages > 0)
|
||||
currentPage = totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPageChanged(int page) => currentPage = page;
|
||||
|
||||
private static string GetStatusBadgeClass(PurchaseItemStatus status) => status switch
|
||||
{
|
||||
PurchaseItemStatus.Pending => "bg-warning text-dark",
|
||||
PurchaseItemStatus.Ordered => "bg-primary",
|
||||
PurchaseItemStatus.Received => "bg-success",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
|
||||
private static bool Contains(string? value, string search) =>
|
||||
value != null && value.Contains(search, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
@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)" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteOffering(offering)" title="Delete"><i class="bi bi-trash"></i></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
@page "/suppliers"
|
||||
@inject SupplierService SupplierService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>Suppliers</PageTitle>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Suppliers</h1>
|
||||
<a href="suppliers/new" class="btn btn-primary">Add Supplier</a>
|
||||
</div>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else if (suppliers.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
No suppliers found. <a href="suppliers/new">Add your first supplier</a>.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Contact Info</th>
|
||||
<th>Notes</th>
|
||||
<th style="width: 100px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var supplier in pagedSuppliers)
|
||||
{
|
||||
<tr>
|
||||
<td><a href="suppliers/@supplier.Id">@supplier.Name</a></td>
|
||||
<td>@supplier.ContactInfo</td>
|
||||
<td>@TruncateText(supplier.Notes, 50)</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="suppliers/@supplier.Id" class="btn btn-sm btn-outline-primary" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(supplier)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pager TotalCount="suppliers.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||
}
|
||||
|
||||
<ConfirmDialog @ref="deleteDialog"
|
||||
Title="Delete Supplier"
|
||||
Message="@deleteMessage"
|
||||
ConfirmText="Delete"
|
||||
OnConfirm="DeleteConfirmed" />
|
||||
|
||||
@code {
|
||||
private List<Supplier> suppliers = new();
|
||||
private bool loading = true;
|
||||
private int currentPage = 1;
|
||||
private int pageSize = 25;
|
||||
private ConfirmDialog deleteDialog = null!;
|
||||
private Supplier? supplierToDelete;
|
||||
private string deleteMessage = "";
|
||||
|
||||
private IEnumerable<Supplier> pagedSuppliers => suppliers.Skip((currentPage - 1) * pageSize).Take(pageSize);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
suppliers = await SupplierService.GetAllAsync();
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private void ConfirmDelete(Supplier supplier)
|
||||
{
|
||||
supplierToDelete = supplier;
|
||||
deleteMessage = $"Are you sure you want to delete \"{supplier.Name}\"? This will also remove all stock lengths associated with this supplier.";
|
||||
deleteDialog.Show();
|
||||
}
|
||||
|
||||
private async Task DeleteConfirmed()
|
||||
{
|
||||
if (supplierToDelete != null)
|
||||
{
|
||||
await SupplierService.DeleteAsync(supplierToDelete.Id);
|
||||
suppliers = await SupplierService.GetAllAsync();
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)suppliers.Count / pageSize);
|
||||
if (currentPage > totalPages && totalPages > 0)
|
||||
currentPage = totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPageChanged(int page) => currentPage = page;
|
||||
|
||||
private string? TruncateText(string? text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
|
||||
return text;
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user