feat: Add purchase order flow with Orders pages
Add "Add to Order List" button on Results page that creates PurchaseItems from optimization results and locks the job. Add Orders Index page with tabbed view (Pending/Ordered/All), supplier assignment, status transitions, and MaterialFilter. Add manual order item creation page. Add Orders link to navigation menu. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,11 @@
|
||||
<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
|
||||
|
||||
@@ -50,6 +50,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-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-cart-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-cart3' viewBox='0 0 16 16'%3E%3Cpath d='M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l.84 4.479 9.144-.459L13.89 4H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%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");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@inject CutListPackingService PackingService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
@inject PurchaseItemService PurchaseItemService
|
||||
@inject StockItemService StockItemService
|
||||
@using CutList.Core
|
||||
@using CutList.Core.Nesting
|
||||
@using CutList.Core.Formatting
|
||||
@@ -21,7 +23,13 @@ else
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1>@job.DisplayName</h1>
|
||||
<h1>
|
||||
@job.DisplayName
|
||||
@if (job.IsLocked)
|
||||
{
|
||||
<i class="bi bi-lock-fill text-warning ms-2" title="Job locked — materials ordered"></i>
|
||||
}
|
||||
</h1>
|
||||
@if (!string.IsNullOrWhiteSpace(job.Customer))
|
||||
{
|
||||
<p class="text-muted mb-0">Customer: @job.Customer</p>
|
||||
@@ -115,7 +123,29 @@ else
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>@summary.TotalToBePurchasedBins bars</h3>
|
||||
<p class="text-muted mb-0">Need to order from supplier</p>
|
||||
@if (summary.TotalToBePurchasedBins > 0)
|
||||
{
|
||||
@if (addedToOrderList)
|
||||
{
|
||||
<div class="alert alert-success mb-0 mt-2 py-2">
|
||||
Added to order list. <a href="orders">View Orders</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-warning btn-sm mt-2" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
||||
@if (addingToOrderList)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
}
|
||||
<i class="bi bi-cart-plus"></i> Add to Order List
|
||||
</button>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mb-0">Need to order from supplier</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,6 +225,9 @@ else
|
||||
private MultiMaterialPackingSummary? summary;
|
||||
private bool loading = true;
|
||||
|
||||
private bool addingToOrderList;
|
||||
private bool addedToOrderList;
|
||||
|
||||
private bool CanOptimize => job != null &&
|
||||
job.Parts.Count > 0 &&
|
||||
job.CuttingToolId != null;
|
||||
@@ -209,6 +242,7 @@ else
|
||||
// Pass job stock if configured, otherwise packing service uses all available stock
|
||||
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
||||
summary = PackingService.GetSummary(packResult);
|
||||
addedToOrderList = job.IsLocked;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
@@ -250,6 +284,58 @@ else
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task AddToOrderList()
|
||||
{
|
||||
addingToOrderList = true;
|
||||
try
|
||||
{
|
||||
var purchaseItems = new List<PurchaseItem>();
|
||||
var stockItems = await StockItemService.GetAllAsync();
|
||||
|
||||
foreach (var materialResult in packResult!.MaterialResults)
|
||||
{
|
||||
if (materialResult.ToBePurchasedBins.Count == 0) continue;
|
||||
|
||||
var materialId = materialResult.Material.Id;
|
||||
|
||||
// Group bins by length to consolidate quantities
|
||||
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
|
||||
{
|
||||
var lengthInches = (decimal)group.Key;
|
||||
var quantity = group.Count();
|
||||
|
||||
// Find the matching stock item
|
||||
var stockItem = stockItems.FirstOrDefault(s =>
|
||||
s.MaterialId == materialId && s.LengthInches == lengthInches);
|
||||
|
||||
if (stockItem != null)
|
||||
{
|
||||
purchaseItems.Add(new PurchaseItem
|
||||
{
|
||||
StockItemId = stockItem.Id,
|
||||
Quantity = quantity,
|
||||
JobId = Id,
|
||||
Status = PurchaseItemStatus.Pending
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (purchaseItems.Count > 0)
|
||||
{
|
||||
await PurchaseItemService.CreateBulkAsync(purchaseItems);
|
||||
}
|
||||
|
||||
await JobService.LockAsync(Id);
|
||||
job = await JobService.GetByIdAsync(Id);
|
||||
addedToOrderList = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
addingToOrderList = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrintReport()
|
||||
{
|
||||
var filename = $"CutList - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
||||
|
||||
151
CutList.Web/Components/Pages/Orders/Add.razor
Normal file
151
CutList.Web/Components/Pages/Orders/Add.razor
Normal file
@@ -0,0 +1,151 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
CutList.Web/Components/Pages/Orders/Index.razor
Normal file
291
CutList.Web/Components/Pages/Orders/Index.razor
Normal file
@@ -0,0 +1,291 @@
|
||||
@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);
|
||||
}
|
||||
Reference in New Issue
Block a user