# Remove Vendor/Supplier Data Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Remove `Supplier`, `SupplierOffering`, and `PurchaseItem` (the Orders workflow) from CutList entirely, strip `StockTransaction` down to a vendor-free quantity log, and fix the bug that started this — an existing stock item's length field is hard-locked `readonly` in the UI. **Architecture:** This is a vertical removal across a Blazor Server + EF Core app. To keep the build green at every step, consumers are updated/deleted before the entities they depend on: UI pages and controllers that reference `Supplier`/`SupplierOffering`/`PurchaseItem` are stripped or deleted first (Tasks 1–6), then the entities, `ApplicationDbContext` config, and EF migration are removed last (Task 7), once nothing references them. `CutList.Mcp` and seed data cleanup follow (Tasks 8–9), and a final build + manual smoke test closes it out (Task 10). **Tech Stack:** .NET 8 Blazor Server (CutList.Web), EF Core + SQL Server, .NET 10 MCP stdio server (CutList.Mcp), Python (one-off catalog transform only). ## Global Constraints - No automated test project exists in this solution (confirmed: `CutList.sln` has 4 projects, none named `*Tests*`). Verification is `dotnet build CutList.sln` after each task, plus manual smoke testing via the running app for UI-facing tasks (per project convention — see `docs/webapp-testing` skill for Playwright-driven checks if needed). - Per global `CLAUDE.md`: apply EF Core migrations immediately after creating them (`dotnet ef database update`) — do not wait for user confirmation. - Per global `CLAUDE.md`: MCP servers are published to `~/.claude/mcp//` via `dotnet publish -c Release -o`. - The migration in Task 7 drops the `Suppliers`, `SupplierOfferings`, and `PurchaseItems` tables and columns outright — this is destructive to whatever's currently in the local dev DB (`Server=localhost\SQLEXPRESS;Database=CutListDb`). That's expected and already agreed in the design doc (`docs/superpowers/specs/2026-07-31-remove-vendor-data-design.md`). - Design source of truth: `docs/superpowers/specs/2026-07-31-remove-vendor-data-design.md`. If any task here conflicts with it, the spec wins — stop and reconcile before proceeding. --- ## File Structure **Modified:** - `CutList.Web/Services/StockItemService.cs` — drop supplier/price params and cost-tracking methods - `CutList.Web/DTOs/StockItemDtos.cs` — drop `StockPricingDto`, supplier/price fields - `CutList.Web/Controllers/StockItemsController.cs` — drop `/offerings`, `/pricing` endpoints - `CutList.Web/Components/Pages/Stock/Edit.razor` — editable length, strip supplier/offerings/pricing UI - `CutList.Web/Components/Layout/NavMenu.razor` — drop Orders/Suppliers links - `CutList.Web/Components/Pages/Home.razor` — drop Suppliers card, update copy - `CutList.Web/Components/Pages/Jobs/Edit.razor` — "Add to Order List" → "Lock Job" - `CutList.Web/Program.cs` — drop `SupplierService`/`PurchaseItemService` registrations - `CutList.Web/DTOs/CatalogDtos.cs` — drop supplier/offering DTOs and fields - `CutList.Web/Services/CatalogService.cs` — drop supplier import/export - `CutList.Web/Data/Entities/StockTransaction.cs` — drop `SupplierId`/`UnitPrice`/`Supplier` nav - `CutList.Web/Data/Entities/StockItem.cs` — drop `SupplierOfferings` nav - `CutList.Web/Data/ApplicationDbContext.cs` — drop `Supplier`/`SupplierOffering`/`PurchaseItem` config and `DbSet`s - `CutList.Web/Data/SeedData/oneals-catalog.json` — regenerated without vendor wrapper - `CutList.Mcp/InventoryTools.cs` — drop supplier tools, add `add_stock` - `CutList.Mcp/ApiClient.cs` — drop supplier/offering API methods and DTOs **Deleted:** - `CutList.Web/Data/Entities/Supplier.cs` - `CutList.Web/Data/Entities/SupplierOffering.cs` - `CutList.Web/Data/Entities/PurchaseItem.cs` - `CutList.Web/Services/SupplierService.cs` - `CutList.Web/Services/PurchaseItemService.cs` - `CutList.Web/Controllers/SuppliersController.cs` - `CutList.Web/DTOs/SupplierDtos.cs` - `CutList.Web/Components/Pages/Suppliers/Index.razor` - `CutList.Web/Components/Pages/Suppliers/Edit.razor` - `CutList.Web/Components/Pages/Orders/Index.razor` - `CutList.Web/Components/Pages/Orders/Add.razor` **Created:** - One new EF Core migration under `CutList.Web/Migrations/` (name: `RemoveVendorData`, generated by the `dotnet ef migrations add` command, not hand-written) --- ### Task 1: StockItemService + StockItemDtos + StockItemsController — strip supplier/pricing **Files:** - Modify: `CutList.Web/Services/StockItemService.cs` - Modify: `CutList.Web/DTOs/StockItemDtos.cs` - Modify: `CutList.Web/Controllers/StockItemsController.cs` **Interfaces:** - Produces: `StockItemService.AddStockAsync(int stockItemId, int quantity, string? notes = null)` — Task 2 (`Stock/Edit.razor`) calls this. - Produces: `StockItemService.GetTransactionHistoryAsync(int stockItemId, int? limit = null)` returning `StockTransaction` with `Job` included but no `Supplier` — Task 2 consumes this for the transaction history table. - Removes: `StockItemService.GetAverageCostAsync`, `GetLastPurchasePriceAsync`, `StockPricingDto` — nothing later in this plan uses them. - [ ] **Step 1: Replace `StockItemService.cs`** Replace the entire file content with: ```csharp using CutList.Web.Data; using CutList.Web.Data.Entities; using Microsoft.EntityFrameworkCore; namespace CutList.Web.Services; public class StockItemService { private readonly IDbContextFactory _factory; public StockItemService(IDbContextFactory factory) { _factory = factory; } public async Task> GetAllAsync(bool includeInactive = false) { await using var context = _factory.CreateDbContext(); var query = context.StockItems .Include(s => s.Material) .AsQueryable(); if (!includeInactive) { query = query.Where(s => s.IsActive); } return await query .OrderBy(s => s.Material.Shape) .ThenBy(s => s.Material.Size) .ThenBy(s => s.LengthInches) .ToListAsync(); } public async Task> GetByMaterialAsync(int materialId, bool includeInactive = false) { await using var context = _factory.CreateDbContext(); var query = context.StockItems .Include(s => s.Material) .Where(s => s.MaterialId == materialId); if (!includeInactive) { query = query.Where(s => s.IsActive); } return await query .OrderBy(s => s.LengthInches) .ToListAsync(); } public async Task GetByIdAsync(int id) { await using var context = _factory.CreateDbContext(); return await context.StockItems .Include(s => s.Material) .FirstOrDefaultAsync(s => s.Id == id); } public async Task CreateAsync(StockItem stockItem) { await using var context = _factory.CreateDbContext(); stockItem.CreatedAt = DateTime.UtcNow; context.StockItems.Add(stockItem); await context.SaveChangesAsync(); return stockItem; } public async Task UpdateAsync(StockItem stockItem) { await using var context = _factory.CreateDbContext(); stockItem.UpdatedAt = DateTime.UtcNow; context.StockItems.Update(stockItem); await context.SaveChangesAsync(); } public async Task DeleteAsync(int id) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(id); if (stockItem != null) { stockItem.IsActive = false; stockItem.UpdatedAt = DateTime.UtcNow; await context.SaveChangesAsync(); } } public async Task ExistsAsync(int materialId, decimal lengthInches, int? excludeId = null) { await using var context = _factory.CreateDbContext(); var query = context.StockItems.Where(s => s.MaterialId == materialId && s.LengthInches == lengthInches && s.IsActive); if (excludeId.HasValue) { query = query.Where(s => s.Id != excludeId.Value); } return await query.AnyAsync(); } // Stock transaction methods public async Task AddStockAsync(int stockItemId, int quantity, string? notes = null) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(stockItemId) ?? throw new InvalidOperationException($"Stock item {stockItemId} not found"); var transaction = new StockTransaction { StockItemId = stockItemId, Quantity = quantity, Type = StockTransactionType.Received, Notes = notes, CreatedAt = DateTime.UtcNow }; stockItem.QuantityOnHand += quantity; stockItem.UpdatedAt = DateTime.UtcNow; context.StockTransactions.Add(transaction); await context.SaveChangesAsync(); return transaction; } public async Task UseStockAsync(int stockItemId, int quantity, int? jobId = null, string? notes = null) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(stockItemId) ?? throw new InvalidOperationException($"Stock item {stockItemId} not found"); var transaction = new StockTransaction { StockItemId = stockItemId, Quantity = -quantity, Type = StockTransactionType.Used, JobId = jobId, Notes = notes, CreatedAt = DateTime.UtcNow }; stockItem.QuantityOnHand -= quantity; stockItem.UpdatedAt = DateTime.UtcNow; context.StockTransactions.Add(transaction); await context.SaveChangesAsync(); return transaction; } public async Task AdjustStockAsync(int stockItemId, int newQuantity, string? notes = null) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(stockItemId) ?? throw new InvalidOperationException($"Stock item {stockItemId} not found"); var difference = newQuantity - stockItem.QuantityOnHand; var transaction = new StockTransaction { StockItemId = stockItemId, Quantity = difference, Type = StockTransactionType.Adjustment, Notes = notes ?? "Manual adjustment", CreatedAt = DateTime.UtcNow }; stockItem.QuantityOnHand = newQuantity; stockItem.UpdatedAt = DateTime.UtcNow; context.StockTransactions.Add(transaction); await context.SaveChangesAsync(); return transaction; } public async Task ScrapStockAsync(int stockItemId, int quantity, string? notes = null) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(stockItemId) ?? throw new InvalidOperationException($"Stock item {stockItemId} not found"); var transaction = new StockTransaction { StockItemId = stockItemId, Quantity = -quantity, Type = StockTransactionType.Scrapped, Notes = notes, CreatedAt = DateTime.UtcNow }; stockItem.QuantityOnHand -= quantity; stockItem.UpdatedAt = DateTime.UtcNow; context.StockTransactions.Add(transaction); await context.SaveChangesAsync(); return transaction; } public async Task> GetTransactionHistoryAsync(int stockItemId, int? limit = null) { await using var context = _factory.CreateDbContext(); var query = context.StockTransactions .Include(t => t.Job) .Where(t => t.StockItemId == stockItemId) .OrderByDescending(t => t.CreatedAt) .AsQueryable(); if (limit.HasValue) { query = query.Take(limit.Value); } return await query.ToListAsync(); } public async Task RecalculateQuantityAsync(int stockItemId) { await using var context = _factory.CreateDbContext(); var stockItem = await context.StockItems.FindAsync(stockItemId) ?? throw new InvalidOperationException($"Stock item {stockItemId} not found"); var calculatedQuantity = await context.StockTransactions .Where(t => t.StockItemId == stockItemId) .SumAsync(t => t.Quantity); if (stockItem.QuantityOnHand != calculatedQuantity) { stockItem.QuantityOnHand = calculatedQuantity; stockItem.UpdatedAt = DateTime.UtcNow; await context.SaveChangesAsync(); } return calculatedQuantity; } } ``` - [ ] **Step 2: Replace `StockItemDtos.cs`** Replace the entire file content with: ```csharp namespace CutList.Web.DTOs; public class StockItemDto { public int Id { get; set; } public int MaterialId { get; set; } public string MaterialName { get; set; } = string.Empty; public decimal LengthInches { get; set; } public string LengthFormatted { get; set; } = string.Empty; public string? Name { get; set; } public int QuantityOnHand { get; set; } public string? Notes { get; set; } public bool IsActive { get; set; } } public class CreateStockItemDto { public int MaterialId { get; set; } public string Length { get; set; } = string.Empty; public string? Name { get; set; } public int QuantityOnHand { get; set; } public string? Notes { get; set; } } public class UpdateStockItemDto { public string? Length { get; set; } public string? Name { get; set; } public string? Notes { get; set; } } public class StockTransactionDto { public int Id { get; set; } public int StockItemId { get; set; } public int Quantity { get; set; } public string Type { get; set; } = string.Empty; public int? JobId { get; set; } public string? JobNumber { get; set; } public string? Notes { get; set; } public DateTime CreatedAt { get; set; } } public class AddStockDto { public int Quantity { get; set; } public string? Notes { get; set; } } public class UseStockDto { public int Quantity { get; set; } public int? JobId { get; set; } public string? Notes { get; set; } } public class AdjustStockDto { public int NewQuantity { get; set; } public string? Notes { get; set; } } public class ScrapStockDto { public int Quantity { get; set; } public string? Notes { get; set; } } ``` - [ ] **Step 3: Replace `StockItemsController.cs`** Replace the entire file content with: ```csharp using CutList.Core.Formatting; using CutList.Web.Data.Entities; using CutList.Web.DTOs; using CutList.Web.Services; using Microsoft.AspNetCore.Mvc; namespace CutList.Web.Controllers; [ApiController] [Route("api/stock-items")] public class StockItemsController : ControllerBase { private readonly StockItemService _stockItemService; public StockItemsController(StockItemService stockItemService) { _stockItemService = stockItemService; } [HttpGet] public async Task>> GetAll( [FromQuery] bool includeInactive = false, [FromQuery] int? materialId = null) { List items; if (materialId.HasValue) items = await _stockItemService.GetByMaterialAsync(materialId.Value, includeInactive); else items = await _stockItemService.GetAllAsync(includeInactive); return Ok(items.Select(MapToDto).ToList()); } [HttpGet("{id}")] public async Task> GetById(int id) { var item = await _stockItemService.GetByIdAsync(id); if (item == null) return NotFound(); return Ok(MapToDto(item)); } [HttpPost] public async Task> Create(CreateStockItemDto dto) { double lengthInches; try { lengthInches = ArchUnits.ParseToInches(dto.Length); } catch { return BadRequest($"Invalid length format: {dto.Length}"); } var exists = await _stockItemService.ExistsAsync(dto.MaterialId, (decimal)lengthInches); if (exists) return Conflict("A stock item with this material and length already exists"); var stockItem = new StockItem { MaterialId = dto.MaterialId, LengthInches = (decimal)lengthInches, Name = dto.Name, QuantityOnHand = dto.QuantityOnHand, Notes = dto.Notes }; await _stockItemService.CreateAsync(stockItem); // Reload with includes var created = await _stockItemService.GetByIdAsync(stockItem.Id); return CreatedAtAction(nameof(GetById), new { id = stockItem.Id }, MapToDto(created!)); } [HttpPut("{id}")] public async Task> Update(int id, UpdateStockItemDto dto) { var item = await _stockItemService.GetByIdAsync(id); if (item == null) return NotFound(); if (dto.Length != null) { try { item.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length); } catch { return BadRequest($"Invalid length format: {dto.Length}"); } } if (dto.Name != null) item.Name = dto.Name; if (dto.Notes != null) item.Notes = dto.Notes; await _stockItemService.UpdateAsync(item); return Ok(MapToDto(item)); } [HttpDelete("{id}")] public async Task Delete(int id) { var item = await _stockItemService.GetByIdAsync(id); if (item == null) return NotFound(); await _stockItemService.DeleteAsync(id); return NoContent(); } [HttpGet("by-material/{materialId}")] public async Task>> GetByMaterial(int materialId) { var items = await _stockItemService.GetByMaterialAsync(materialId); return Ok(items.Select(MapToDto).ToList()); } [HttpGet("{id}/transactions")] public async Task>> GetTransactions(int id, [FromQuery] int? limit = null) { var item = await _stockItemService.GetByIdAsync(id); if (item == null) return NotFound(); var transactions = await _stockItemService.GetTransactionHistoryAsync(id, limit); return Ok(transactions.Select(MapTransactionToDto).ToList()); } [HttpPost("{id}/receive")] public async Task> ReceiveStock(int id, AddStockDto dto) { try { var transaction = await _stockItemService.AddStockAsync(id, dto.Quantity, dto.Notes); return Ok(MapTransactionToDto(transaction)); } catch (InvalidOperationException) { return NotFound(); } } [HttpPost("{id}/use")] public async Task> UseStock(int id, UseStockDto dto) { try { var transaction = await _stockItemService.UseStockAsync(id, dto.Quantity, dto.JobId, dto.Notes); return Ok(MapTransactionToDto(transaction)); } catch (InvalidOperationException) { return NotFound(); } } [HttpPost("{id}/adjust")] public async Task> AdjustStock(int id, AdjustStockDto dto) { try { var transaction = await _stockItemService.AdjustStockAsync(id, dto.NewQuantity, dto.Notes); return Ok(MapTransactionToDto(transaction)); } catch (InvalidOperationException) { return NotFound(); } } [HttpPost("{id}/scrap")] public async Task> ScrapStock(int id, ScrapStockDto dto) { try { var transaction = await _stockItemService.ScrapStockAsync(id, dto.Quantity, dto.Notes); return Ok(MapTransactionToDto(transaction)); } catch (InvalidOperationException) { return NotFound(); } } [HttpPost("{id}/recalculate")] public async Task> RecalculateStock(int id) { try { var newQuantity = await _stockItemService.RecalculateQuantityAsync(id); return Ok(new { QuantityOnHand = newQuantity }); } catch (InvalidOperationException) { return NotFound(); } } private static StockItemDto MapToDto(StockItem s) => new() { Id = s.Id, MaterialId = s.MaterialId, MaterialName = s.Material?.DisplayName ?? string.Empty, LengthInches = s.LengthInches, LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches), Name = s.Name, QuantityOnHand = s.QuantityOnHand, Notes = s.Notes, IsActive = s.IsActive }; private static StockTransactionDto MapTransactionToDto(StockTransaction t) => new() { Id = t.Id, StockItemId = t.StockItemId, Quantity = t.Quantity, Type = t.Type.ToString(), JobId = t.JobId, JobNumber = t.Job?.JobNumber, Notes = t.Notes, CreatedAt = t.CreatedAt }; } ``` - [ ] **Step 4: Build to verify no compile errors introduced yet** Run: `dotnet build CutList.sln` Expected: Build FAILS — `Stock/Edit.razor`, `Orders/*.razor`, `CatalogService.cs` etc. still call the old `AddStockAsync`/`GetByIdAsync` overloads and reference `StockPricingDto`. That's expected; Task 2 onward fixes each consumer. Confirm the *only* errors are in those not-yet-updated files (`Stock/Edit.razor`, `Orders/Index.razor`, `Orders/Add.razor`), not in `StockItemsController.cs`/`StockItemService.cs`/`StockItemDtos.cs` themselves. - [ ] **Step 5: Commit** ```bash git add CutList.Web/Services/StockItemService.cs CutList.Web/DTOs/StockItemDtos.cs CutList.Web/Controllers/StockItemsController.cs git commit -m "refactor: strip supplier/pricing from StockItemService and stock-items API" ``` --- ### Task 2: Stock/Edit.razor — editable length + strip supplier/offerings/pricing UI **Files:** - Modify: `CutList.Web/Components/Pages/Stock/Edit.razor` **Interfaces:** - Consumes: `StockItemService.AddStockAsync(int, int, string?)`, `AdjustStockAsync`, `ScrapStockAsync`, `GetTransactionHistoryAsync` from Task 1. - No longer consumes `SupplierService` (any method) — this task removes the injection entirely. - [ ] **Step 1: Replace `Stock/Edit.razor`** This fixes the original reported bug (length field locked `readonly` outside creation) and removes the Supplier Offerings card, the Supplier/Unit Price fields on the transaction form, and the Supplier/Price columns in transaction history. Replace the entire file content with: ```razor @page "/stock/new" @page "/stock/{Id:int}" @inject StockItemService StockItemService @inject MaterialService MaterialService @inject NavigationManager Navigation @using CutList.Core.Formatting @(IsNew ? "Add Stock Item" : "Edit Stock Item")

@(IsNew ? "Add Stock Item" : $"{stockItem.Material?.DisplayName} - {ArchUnits.FormatFromInches((double)stockItem.LengthInches)}")

@if (loading) {

Loading...

} else {
Stock Item Details
@if (!string.IsNullOrEmpty(errorMessage)) {
@errorMessage
}
@(IsNew ? "Cancel" : "Back to List")
@if (!IsNew) {
Inventory @stockItem.QuantityOnHand on hand
@if (showStockForm) {
Stock Transaction
@if (!string.IsNullOrEmpty(stockFormErrorMessage)) {
@stockFormErrorMessage
}
} @if (transactions.Count == 0) {

No transaction history yet.

} else { @foreach (var txn in transactions) { }
Date Type Qty Notes
@txn.CreatedAt.ToLocalTime().ToString("MM/dd/yy HH:mm") @txn.Type @(txn.Quantity >= 0 ? "+" : "")@txn.Quantity @(txn.Notes ?? "-")
}
}
} @code { [Parameter] public int? Id { get; set; } private StockItem stockItem = new(); private List materials = new(); private List transactions = new(); private bool loading = true; private bool saving; private string? errorMessage; // Stock transaction form private bool showStockForm; private bool savingStockTransaction; private string stockTransactionType = "add"; private int stockQuantity; private string? stockNotes; private string? stockFormErrorMessage; private bool IsNew => !Id.HasValue; protected override async Task OnInitializedAsync() { materials = await MaterialService.GetAllAsync(); if (Id.HasValue) { var existing = await StockItemService.GetByIdAsync(Id.Value); if (existing == null) { Navigation.NavigateTo("stock"); return; } stockItem = existing; 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; 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, 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; saving = true; try { if (stockItem.MaterialId == 0) { errorMessage = "Please select a material"; return; } if (stockItem.LengthInches <= 0) { errorMessage = "Length must be greater than zero"; return; } var exists = await StockItemService.ExistsAsync( stockItem.MaterialId, stockItem.LengthInches, IsNew ? null : stockItem.Id); if (exists) { errorMessage = "A stock item with this material and length already exists"; return; } if (IsNew) { var created = await StockItemService.CreateAsync(stockItem); Navigation.NavigateTo($"stock/{created.Id}"); } else { await StockItemService.UpdateAsync(stockItem); } } finally { saving = false; } } } ``` Note: `Material`, `StockItem`, `StockTransaction`, and `StockTransactionType` resolve without an explicit `@using` — `CutList.Web.Data.Entities` and `CutList.Web.Services` are both global usings declared in `CutList.Web/Components/_Imports.razor`, which applies to every `.razor` file under `Components/`. - [ ] **Step 2: Build to verify this file compiles clean** Run: `dotnet build CutList.sln` Expected: No errors originating from `Stock/Edit.razor`. Errors may still remain in `Orders/*.razor`, `CatalogService.cs` (fixed in later tasks). - [ ] **Step 3: Commit** ```bash git add CutList.Web/Components/Pages/Stock/Edit.razor git commit -m "fix: make stock item length editable and drop vendor UI from Stock Edit page" ``` --- ### Task 3: Delete Suppliers & Orders pages; update NavMenu.razor + Home.razor **Files:** - Delete: `CutList.Web/Components/Pages/Suppliers/Index.razor` - Delete: `CutList.Web/Components/Pages/Suppliers/Edit.razor` - Delete: `CutList.Web/Components/Pages/Orders/Index.razor` - Delete: `CutList.Web/Components/Pages/Orders/Add.razor` - Modify: `CutList.Web/Components/Layout/NavMenu.razor` - Modify: `CutList.Web/Components/Pages/Home.razor` **Interfaces:** - None — this task only removes UI entry points. `SupplierService`/`PurchaseItemService` are still registered at this point (deleted in Task 5) so the build stays green even though these two services now have zero UI consumers left in `CutList.Web` other than `Jobs/Edit.razor` (fixed in Task 4) and `CatalogService`/`StockItemsController` (already fixed in Tasks 1 and 6). - [ ] **Step 1: Delete the four Razor files** ```bash git rm CutList.Web/Components/Pages/Suppliers/Index.razor git rm CutList.Web/Components/Pages/Suppliers/Edit.razor git rm CutList.Web/Components/Pages/Orders/Index.razor git rm CutList.Web/Components/Pages/Orders/Add.razor ``` - [ ] **Step 2: Replace `NavMenu.razor`** Replace the entire file content with: ```razor ``` - [ ] **Step 3: Replace `Home.razor`** Replace the entire file content with: ```razor @page "/" CutList - Home

CutList

1D Bin Packing Optimization for Material Cutting

Jobs

Create and manage cut list jobs. Add parts and stock bins, then optimize to minimize waste.

Go to Jobs
Materials

Manage material types (tube, bar, angle, etc.) with their shapes and sizes.

Manage Materials
Cutting Tools

Configure cutting tools with their kerf widths for accurate waste calculations.

Manage Tools

How It Works

  1. Set up materials - Define the shapes and sizes of materials you work with
  2. Add stock - Record which stock lengths you have on hand
  3. Create a job - Add the parts you need to cut with their lengths and quantities
  4. Add stock bins - Specify which stock lengths to cut from
  5. Optimize - Run the optimizer to find the best cutting pattern
  6. Print report - Generate a printable cut list to take to the shop
``` - [ ] **Step 4: Build to verify** Run: `dotnet build CutList.sln` Expected: The four deleted files no longer appear in errors. Remaining errors (if any) should only be in `Jobs/Edit.razor` and `CatalogService.cs` (fixed in Tasks 4 and 6). - [ ] **Step 5: Commit** ```bash git add -A CutList.Web/Components/Pages/Suppliers CutList.Web/Components/Pages/Orders CutList.Web/Components/Layout/NavMenu.razor CutList.Web/Components/Pages/Home.razor git commit -m "feat: remove Suppliers and Orders pages and nav entries" ``` --- ### Task 4: Jobs/Edit.razor — replace "Add to Order List" with "Lock Job" **Files:** - Modify: `CutList.Web/Components/Pages/Jobs/Edit.razor` **Interfaces:** - Consumes: `JobService.LockAsync(int id)` / `UnlockAsync(int id)` — already exist, unchanged, from `CutList.Web/Services/JobService.cs`. - No longer consumes `PurchaseItemService` (any method). - [ ] **Step 1: Drop the `PurchaseItemService` injection** In `CutList.Web/Components/Pages/Jobs/Edit.razor`, find: ```razor @inject CutListPackingService PackingService @inject PurchaseItemService PurchaseItemService @inject NavigationManager Navigation ``` Replace with: ```razor @inject CutListPackingService PackingService @inject NavigationManager Navigation ``` - [ ] **Step 2: Rename the tracking fields** Find: ```csharp private bool addingToOrderList; private bool addedToOrderList; ``` Replace with: ```csharp private bool lockingJob; private bool jobLocked; ``` - [ ] **Step 3: Update `LoadSavedResultsAsync`** Find: ```csharp summary = PackingService.GetSummary(packResult); addedToOrderList = job.IsLocked; } } catch { // Invalid JSON — treat as no results ``` Replace with: ```csharp summary = PackingService.GetSummary(packResult); jobLocked = job.IsLocked; } } catch { // Invalid JSON — treat as no results ``` - [ ] **Step 4: Replace `AddToOrderList` with `LockJob`** Find: ```csharp // Refresh job to get updated OptimizedAt job = (await JobService.GetByIdAsync(Id!.Value))!; addedToOrderList = job.IsLocked; } finally { optimizing = false; } } private async Task AddToOrderList() { addingToOrderList = true; try { var purchaseItems = new List(); 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!.Value, Status = PurchaseItemStatus.Pending }); } } } if (purchaseItems.Count > 0) { await PurchaseItemService.CreateBulkAsync(purchaseItems); } await JobService.LockAsync(Id!.Value); job = (await JobService.GetByIdAsync(Id!.Value))!; addedToOrderList = true; } finally { addingToOrderList = false; } } ``` Replace with: ```csharp // Refresh job to get updated OptimizedAt job = (await JobService.GetByIdAsync(Id!.Value))!; jobLocked = job.IsLocked; } finally { optimizing = false; } } private async Task LockJob() { lockingJob = true; try { await JobService.LockAsync(Id!.Value); job = (await JobService.GetByIdAsync(Id!.Value))!; jobLocked = true; } finally { lockingJob = false; } } ``` - [ ] **Step 5: Update the Purchase List card markup** Find: ```razor