Compare commits
14
Commits
7574476d7f
...
7b210f518b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b210f518b | ||
|
|
3244e15593 | ||
|
|
b77d0d971d | ||
|
|
f75c205bc5 | ||
|
|
1b4d5f76a8 | ||
|
|
41944638c6 | ||
|
|
063c844310 | ||
|
|
597181bd78 | ||
|
|
8d6ada35e5 | ||
|
|
b51d936b78 | ||
|
|
e7248be9a4 | ||
|
|
06f0c2032d | ||
|
|
9296bea0bc | ||
|
|
4e874df22f |
+7
-1
@@ -247,4 +247,10 @@ ModelManifest.xml
|
|||||||
# AlroCatalog scraper output
|
# AlroCatalog scraper output
|
||||||
scripts/AlroCatalog/__pycache__/
|
scripts/AlroCatalog/__pycache__/
|
||||||
scripts/AlroCatalog/pdfs/
|
scripts/AlroCatalog/pdfs/
|
||||||
scripts/AlroCatalog/screenshots/
|
scripts/AlroCatalog/screenshots/
|
||||||
|
|
||||||
|
# Superpowers subagent-driven-development scratch workspace
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
|
# Playwright MCP browser session logs/traces
|
||||||
|
.playwright-mcp/
|
||||||
@@ -91,9 +91,9 @@ CutList.Mcp is an stdio MCP server, not a hosted service — it's published to `
|
|||||||
|
|
||||||
**Database**: SQL Server via Entity Framework Core (connection string: `DefaultConnection`)
|
**Database**: SQL Server via Entity Framework Core (connection string: `DefaultConnection`)
|
||||||
|
|
||||||
**Service Registration** (Program.cs): All services registered as Scoped — MaterialService, SupplierService, StockItemService, JobService, CutListPackingService, ReportService, PurchaseItemService, CatalogService. `IDbContextFactory<ApplicationDbContext>` is used (not a scoped `DbContext` directly) for Blazor Server circuit safety.
|
**Service Registration** (Program.cs): All services registered as Scoped — MaterialService, StockItemService, JobService, CutListPackingService, ReportService, CatalogService. `IDbContextFactory<ApplicationDbContext>` is used (not a scoped `DbContext` directly) for Blazor Server circuit safety.
|
||||||
|
|
||||||
**REST API** (`Controllers/`): `JobsController`, `MaterialsController`, `StockItemsController`, `SuppliersController`, `CuttingToolsController`, `PackingController`, `CatalogController` — Swagger/OpenAPI enabled in Development. This API is the integration surface `CutList.Mcp` calls into; the Blazor UI talks to the services directly and does not go through it.
|
**REST API** (`Controllers/`): `JobsController`, `MaterialsController`, `StockItemsController`, `CuttingToolsController`, `PackingController`, `CatalogController` — Swagger/OpenAPI enabled in Development. This API is the integration surface `CutList.Mcp` calls into; the Blazor UI talks to the services directly and does not go through it.
|
||||||
|
|
||||||
**Error handling**: `UseExceptionHandler("/Error", ...)` in non-Development environments routes to `Components/Pages/Error.razor`.
|
**Error handling**: `UseExceptionHandler("/Error", ...)` in non-Development environments routes to `Components/Pages/Error.razor`.
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ Stdio-transport MCP server (`ModelContextProtocol` SDK) exposing CutList.Web's R
|
|||||||
|
|
||||||
- `ApiClient.cs` — typed `HttpClient` wrapper for CutList.Web's REST API (`BaseAddress` hardcoded to `http://localhost:5270`)
|
- `ApiClient.cs` — typed `HttpClient` wrapper for CutList.Web's REST API (`BaseAddress` hardcoded to `http://localhost:5270`)
|
||||||
- `JobTools.cs` — job CRUD, parts/stock, optimization (`OptimizeJob`), cutting tools
|
- `JobTools.cs` — job CRUD, parts/stock, optimization (`OptimizeJob`), cutting tools
|
||||||
- `InventoryTools.cs` — suppliers, materials, stock items, supplier offerings
|
- `InventoryTools.cs` — materials, stock items (`add_stock`, etc.)
|
||||||
- `CutListTools.cs` — static helpers shared across tool classes
|
- `CutListTools.cs` — static helpers shared across tool classes
|
||||||
- `Models.cs` — shared DTOs distinct from CutList.Web's own DTOs (kept intentionally thin for MCP tool responses)
|
- `Models.cs` — shared DTOs distinct from CutList.Web's own DTOs (kept intentionally thin for MCP tool responses)
|
||||||
|
|
||||||
@@ -122,19 +122,11 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
### StockItem
|
### StockItem
|
||||||
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`
|
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`
|
||||||
- **Unique constraint**: (MaterialId, LengthInches)
|
- **Unique constraint**: (MaterialId, LengthInches)
|
||||||
- **Relationships**: `Material`, `SupplierOfferings` (1:many), `Transactions` (1:many StockTransaction)
|
- **Relationships**: `Material`, `Transactions` (1:many StockTransaction)
|
||||||
|
|
||||||
### StockTransaction
|
### StockTransaction
|
||||||
- `StockItemId`, `Quantity` (signed delta), `Type` (Received/Used/Adjustment/Scrapped/Returned)
|
- `StockItemId`, `Quantity` (signed delta), `Type` (Received/Used/Adjustment/Scrapped/Returned)
|
||||||
- Optional: `JobId`, `SupplierId`, `UnitPrice`
|
- Optional: `JobId`
|
||||||
|
|
||||||
### Supplier
|
|
||||||
- `Name` (required), `ContactInfo`, `Notes`, `IsActive`
|
|
||||||
- **Relationships**: `Offerings` (1:many SupplierOffering)
|
|
||||||
|
|
||||||
### SupplierOffering
|
|
||||||
- Links Supplier to StockItem with optional `PartNumber`, `Price`, `Notes`
|
|
||||||
- **Unique constraint**: (SupplierId, StockItemId)
|
|
||||||
|
|
||||||
### CuttingTool
|
### CuttingTool
|
||||||
- `Name`, `KerfInches` (decimal), `IsDefault` (bool), `IsActive`
|
- `Name`, `KerfInches` (decimal), `IsDefault` (bool), `IsActive`
|
||||||
@@ -153,9 +145,6 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
### JobStock
|
### JobStock
|
||||||
- `JobId`, `MaterialId`, `StockItemId?`, `LengthInches`, `Quantity` (-1 = unlimited), `IsCustomLength`, `Priority` (lower = used first), `SortOrder`
|
- `JobId`, `MaterialId`, `StockItemId?`, `LengthInches`, `Quantity` (-1 = unlimited), `IsCustomLength`, `Priority` (lower = used first), `SortOrder`
|
||||||
|
|
||||||
### PurchaseItem
|
|
||||||
- `StockItemId`, `SupplierId?`, `JobId?`, `Quantity`, `Status` (Pending/Ordered/Received), `Notes`
|
|
||||||
|
|
||||||
## CutList.Web Services
|
## CutList.Web Services
|
||||||
|
|
||||||
### MaterialService
|
### MaterialService
|
||||||
@@ -167,15 +156,10 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
- CRUD with soft delete
|
- CRUD with soft delete
|
||||||
- Stock transactions: `AddStockAsync`, `UseStockAsync`, `AdjustStockAsync`, `ScrapStockAsync`
|
- Stock transactions: `AddStockAsync`, `UseStockAsync`, `AdjustStockAsync`, `ScrapStockAsync`
|
||||||
- `GetTransactionHistoryAsync`, `RecalculateQuantityAsync`
|
- `GetTransactionHistoryAsync`, `RecalculateQuantityAsync`
|
||||||
- Pricing: `GetAverageCostAsync`, `GetLastPurchasePriceAsync`
|
|
||||||
|
|
||||||
### SupplierService
|
|
||||||
- CRUD for suppliers and offerings
|
|
||||||
- `GetOfferingsForStockItemAsync` — all supplier options for a stock item
|
|
||||||
|
|
||||||
### JobService
|
### JobService
|
||||||
- Job CRUD: `CreateAsync` (auto-generates JobNumber), `DuplicateAsync` (deep copy), `QuickCreateAsync`
|
- Job CRUD: `CreateAsync` (auto-generates JobNumber), `DuplicateAsync` (deep copy), `QuickCreateAsync`
|
||||||
- Lock/Unlock: `LockAsync(id)`, `UnlockAsync(id)` — controls job editability after ordering
|
- Lock/Unlock: `LockAsync(id)`, `UnlockAsync(id)` — controls job editability
|
||||||
- Parts: `AddPartAsync`, `UpdatePartAsync`, `DeletePartAsync` (all update job timestamp + clear optimization results)
|
- Parts: `AddPartAsync`, `UpdatePartAsync`, `DeletePartAsync` (all update job timestamp + clear optimization results)
|
||||||
- Stock: `AddStockAsync`, `UpdateStockAsync`, `DeleteStockAsync` (all clear optimization results)
|
- Stock: `AddStockAsync`, `UpdateStockAsync`, `DeleteStockAsync` (all clear optimization results)
|
||||||
- Optimization: `SaveOptimizationResultAsync`, `ClearOptimizationResultAsync`
|
- Optimization: `SaveOptimizationResultAsync`, `ClearOptimizationResultAsync`
|
||||||
@@ -187,15 +171,11 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
||||||
- `SerializeResult(result)` / `LoadSavedResult(json)` — JSON round-trip via DTO layer (`SavedOptimizationResult` etc.)
|
- `SerializeResult(result)` / `LoadSavedResult(json)` — JSON round-trip via DTO layer (`SavedOptimizationResult` etc.)
|
||||||
|
|
||||||
### PurchaseItemService
|
|
||||||
- CRUD + `CreateBulkAsync` for batch creation from optimization results
|
|
||||||
- `UpdateStatusAsync(id, status)`, `UpdateSupplierAsync(id, supplierId)`
|
|
||||||
|
|
||||||
### ReportService
|
### ReportService
|
||||||
- `FormatLength(inches)`, `GroupItems(items)` for print report formatting
|
- `FormatLength(inches)`, `GroupItems(items)` for print report formatting
|
||||||
|
|
||||||
### CatalogService
|
### CatalogService
|
||||||
- `ExportAsync()` — dumps active suppliers, cutting tools, and materials (with dimensions + stock items + supplier offerings) into a shape-grouped `CatalogData` DTO for bulk export/import tooling
|
- `ExportAsync()` — dumps cutting tools and materials (with dimensions + stock items) into a shape-grouped `CatalogData` DTO for bulk export/import tooling
|
||||||
- Backs the `CatalogController` REST endpoint and the `scripts/ExportData` / `scripts/AlroCatalog` data-loading workflows
|
- Backs the `CatalogController` REST endpoint and the `scripts/ExportData` / `scripts/AlroCatalog` data-loading workflows
|
||||||
|
|
||||||
## CutList.Web Pages
|
## CutList.Web Pages
|
||||||
@@ -210,10 +190,6 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
||||||
| `/stock` | Stock/Index | Stock items with MaterialFilter, quantity badges |
|
| `/stock` | Stock/Index | Stock items with MaterialFilter, quantity badges |
|
||||||
| `/stock/new`, `/stock/{Id}` | Stock/Edit | Stock item form |
|
| `/stock/new`, `/stock/{Id}` | Stock/Edit | Stock item form |
|
||||||
| `/orders` | Orders/Index | Tabbed (Pending/Ordered/All), supplier assignment, status transitions |
|
|
||||||
| `/orders/add` | Orders/Add | Manual purchase item creation |
|
|
||||||
| `/suppliers` | Suppliers/Index | Supplier list with CRUD |
|
|
||||||
| `/suppliers/{Id}` | Suppliers/Edit | Supplier + offerings management |
|
|
||||||
| `/tools` | Tools/Index | Cutting tools CRUD |
|
| `/tools` | Tools/Index | Cutting tools CRUD |
|
||||||
| `/Error` | Error | Unhandled exception page (registered via `UseExceptionHandler`) |
|
| `/Error` | Error | Unhandled exception page (registered via `UseExceptionHandler`) |
|
||||||
|
|
||||||
@@ -224,20 +200,20 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its
|
|||||||
| `ConfirmDialog` | Modal confirmation for destructive actions (Show/Hide methods, OnConfirm callback) |
|
| `ConfirmDialog` | Modal confirmation for destructive actions (Show/Hide methods, OnConfirm callback) |
|
||||||
| `LengthInput` | Architectural unit input — parses "12'", "6\"", "12 1/2\""; reformats on blur; two-way binding via `Value` or `NullableValue` |
|
| `LengthInput` | Architectural unit input — parses "12'", "6\"", "12 1/2\""; reformats on blur; two-way binding via `Value` or `NullableValue` |
|
||||||
| `Pager` | Pagination with "Showing X-Y of Z", prev/next, smart page window with ellipsis |
|
| `Pager` | Pagination with "Showing X-Y of Z", prev/next, smart page window with ellipsis |
|
||||||
| `MaterialFilter` | Reusable filter: Shape, Type, Grade dropdowns + search text; used on Materials, Stock, Orders pages |
|
| `MaterialFilter` | Reusable filter: Shape, Type, Grade dropdowns + search text; used on Materials, Stock pages |
|
||||||
|
|
||||||
## Key Patterns & Conventions
|
## Key Patterns & Conventions
|
||||||
|
|
||||||
- **Nullable reference types enabled** — handle nulls explicitly
|
- **Nullable reference types enabled** — handle nulls explicitly
|
||||||
- **Soft deletes** — Materials, Suppliers, StockItems, CuttingTools use `IsActive` flag
|
- **Soft deletes** — Materials, StockItems, CuttingTools use `IsActive` flag
|
||||||
- **Job locking** — `LockedAt` timestamp set when materials ordered; Edit page disables all modification via `<fieldset disabled>`, hides add/edit/delete buttons; Unlock button to re-enable editing
|
- **Job locking** — `LockedAt` timestamp set via a manual Lock Job action (always available, regardless of whether the job needs purchases); Edit page disables all modification via `<fieldset disabled>`, hides add/edit/delete buttons; Unlock button to re-enable editing
|
||||||
- **Pagination** — All list pages use `Pager` with `pageSize = 25`
|
- **Pagination** — All list pages use `Pager` with `pageSize = 25`
|
||||||
- **ConfirmDialog** — All destructive actions use the shared `ConfirmDialog` component
|
- **ConfirmDialog** — All destructive actions use the shared `ConfirmDialog` component
|
||||||
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
||||||
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
||||||
- **Job stock** — Jobs can use auto-discovered inventory OR define custom stock lengths
|
- **Job stock** — Jobs can use auto-discovered inventory OR define custom stock lengths
|
||||||
- **Optimization persistence** — Results saved as JSON in `Job.OptimizationResultJson`; DTO layer (`SavedOptimizationResult` etc.) handles serialization since Core types use encapsulated collections; results auto-cleared when parts, stock, or cutting tool change
|
- **Optimization persistence** — Results saved as JSON in `Job.OptimizationResultJson`; DTO layer (`SavedOptimizationResult` etc.) handles serialization since Core types use encapsulated collections; results auto-cleared when parts, stock, or cutting tool change
|
||||||
- **Purchase flow** — Optimize job -> "Add to Order List" creates PurchaseItems + locks job -> Orders page manages status (Pending -> Ordered -> Received)
|
- **Job lock flow** — Optimize job -> Lock Job (manual action, available whether or not purchases are needed) -> job becomes read-only until Unlock
|
||||||
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
||||||
- **Collections** — Encapsulated in Core; use `AsReadOnly()`, access via `Add*` methods
|
- **Collections** — Encapsulated in Core; use `AsReadOnly()`, access via `Add*` methods
|
||||||
- **Priority system** — Lower priority bins used first in packing algorithm
|
- **Priority system** — Lower priority bins used first in packing algorithm
|
||||||
|
|||||||
@@ -14,23 +14,6 @@ public class ApiClient
|
|||||||
_http = http;
|
_http = http;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Suppliers
|
|
||||||
|
|
||||||
public async Task<List<ApiSupplierDto>> GetSuppliersAsync(bool includeInactive = false)
|
|
||||||
{
|
|
||||||
var url = $"api/suppliers?includeInactive={includeInactive}";
|
|
||||||
return await _http.GetFromJsonAsync<List<ApiSupplierDto>>(url) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ApiSupplierDto?> CreateSupplierAsync(string name, string? contactInfo, string? notes)
|
|
||||||
{
|
|
||||||
var response = await _http.PostAsJsonAsync("api/suppliers", new { Name = name, ContactInfo = contactInfo, Notes = notes });
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
return await response.Content.ReadFromJsonAsync<ApiSupplierDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Materials
|
#region Materials
|
||||||
|
|
||||||
public async Task<List<ApiMaterialDto>> GetMaterialsAsync(string? shape = null, bool includeInactive = false)
|
public async Task<List<ApiMaterialDto>> GetMaterialsAsync(string? shape = null, bool includeInactive = false)
|
||||||
@@ -212,40 +195,6 @@ public class ApiClient
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Offerings
|
|
||||||
|
|
||||||
public async Task<List<ApiOfferingDto>> GetOfferingsForSupplierAsync(int supplierId)
|
|
||||||
{
|
|
||||||
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/suppliers/{supplierId}/offerings") ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<ApiOfferingDto>> GetOfferingsForStockItemAsync(int stockItemId)
|
|
||||||
{
|
|
||||||
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/stock-items/{stockItemId}/offerings") ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ApiOfferingDto?> CreateOfferingAsync(int supplierId, int stockItemId,
|
|
||||||
string? partNumber, string? supplierDescription, decimal? price, string? notes)
|
|
||||||
{
|
|
||||||
var body = new
|
|
||||||
{
|
|
||||||
StockItemId = stockItemId,
|
|
||||||
PartNumber = partNumber,
|
|
||||||
SupplierDescription = supplierDescription,
|
|
||||||
Price = price,
|
|
||||||
Notes = notes
|
|
||||||
};
|
|
||||||
var response = await _http.PostAsJsonAsync($"api/suppliers/{supplierId}/offerings", body);
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
|
|
||||||
{
|
|
||||||
var error = await response.Content.ReadAsStringAsync();
|
|
||||||
throw new ApiConflictException(error);
|
|
||||||
}
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
return await response.Content.ReadFromJsonAsync<ApiOfferingDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -385,15 +334,6 @@ public class ApiMaterialPackingSummaryDto
|
|||||||
|
|
||||||
#region API Response DTOs — Inventory
|
#region API Response DTOs — Inventory
|
||||||
|
|
||||||
public class ApiSupplierDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ApiMaterialDto
|
public class ApiMaterialDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
@@ -425,20 +365,4 @@ public class ApiStockItemDto
|
|||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ApiOfferingDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int SupplierId { get; set; }
|
|
||||||
public string? SupplierName { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public string? MaterialName { get; set; }
|
|
||||||
public decimal? LengthInches { get; set; }
|
|
||||||
public string? LengthFormatted { get; set; }
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
+25
-287
@@ -5,7 +5,7 @@ using ModelContextProtocol.Server;
|
|||||||
namespace CutList.Mcp;
|
namespace CutList.Mcp;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MCP tools for inventory management - suppliers, materials, stock items, and offerings.
|
/// MCP tools for inventory management - materials and stock items.
|
||||||
/// All calls go through the CutList.Web REST API via ApiClient.
|
/// All calls go through the CutList.Web REST API via ApiClient.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
@@ -18,56 +18,6 @@ public class InventoryTools
|
|||||||
_api = api;
|
_api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Suppliers
|
|
||||||
|
|
||||||
[McpServerTool(Name = "list_suppliers"), Description("Lists all suppliers in the system.")]
|
|
||||||
public async Task<SupplierListResult> ListSuppliers(
|
|
||||||
[Description("Include inactive suppliers (default false)")]
|
|
||||||
bool includeInactive = false)
|
|
||||||
{
|
|
||||||
var suppliers = await _api.GetSuppliersAsync(includeInactive);
|
|
||||||
|
|
||||||
return new SupplierListResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Suppliers = suppliers.Select(s => new SupplierDto
|
|
||||||
{
|
|
||||||
Id = s.Id,
|
|
||||||
Name = s.Name,
|
|
||||||
ContactInfo = s.ContactInfo,
|
|
||||||
Notes = s.Notes,
|
|
||||||
IsActive = s.IsActive
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool(Name = "add_supplier"), Description("Adds a new supplier to the system.")]
|
|
||||||
public async Task<SupplierResult> AddSupplier(
|
|
||||||
[Description("Supplier name (e.g., 'O'Neal Steel')")]
|
|
||||||
string name,
|
|
||||||
[Description("Contact info - website, phone, email, etc.")]
|
|
||||||
string? contactInfo = null,
|
|
||||||
[Description("Notes about the supplier")]
|
|
||||||
string? notes = null)
|
|
||||||
{
|
|
||||||
var supplier = await _api.CreateSupplierAsync(name, contactInfo, notes);
|
|
||||||
|
|
||||||
return new SupplierResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Supplier = supplier != null ? new SupplierDto
|
|
||||||
{
|
|
||||||
Id = supplier.Id,
|
|
||||||
Name = supplier.Name,
|
|
||||||
ContactInfo = supplier.ContactInfo,
|
|
||||||
Notes = supplier.Notes,
|
|
||||||
IsActive = supplier.IsActive
|
|
||||||
} : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Materials
|
#region Materials
|
||||||
|
|
||||||
[McpServerTool(Name = "list_materials"), Description("Lists all materials (shape/size combinations) in the system.")]
|
[McpServerTool(Name = "list_materials"), Description("Lists all materials (shape/size combinations) in the system.")]
|
||||||
@@ -285,151 +235,22 @@ public class InventoryTools
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Supplier Offerings
|
#region Convenience
|
||||||
|
|
||||||
[McpServerTool(Name = "list_supplier_offerings"), Description("Lists supplier offerings (what suppliers sell for each stock item).")]
|
[McpServerTool(Name = "add_stock"), Description("Convenience method: adds a material (if needed) and a stock item (if needed) with an initial quantity, all in one call.")]
|
||||||
public async Task<SupplierOfferingListResult> ListSupplierOfferings(
|
public async Task<AddStockResult> AddStock(
|
||||||
[Description("Filter by supplier ID")]
|
|
||||||
int? supplierId = null,
|
|
||||||
[Description("Filter by stock item ID")]
|
|
||||||
int? stockItemId = null,
|
|
||||||
[Description("Filter by material ID")]
|
|
||||||
int? materialId = null)
|
|
||||||
{
|
|
||||||
List<ApiOfferingDto> offerings;
|
|
||||||
|
|
||||||
if (supplierId.HasValue)
|
|
||||||
{
|
|
||||||
offerings = await _api.GetOfferingsForSupplierAsync(supplierId.Value);
|
|
||||||
// Apply additional filters client-side
|
|
||||||
if (stockItemId.HasValue)
|
|
||||||
offerings = offerings.Where(o => o.StockItemId == stockItemId.Value).ToList();
|
|
||||||
if (materialId.HasValue)
|
|
||||||
{
|
|
||||||
// Need to get stock items for this material to filter
|
|
||||||
var stockItems = await _api.GetStockItemsAsync(materialId);
|
|
||||||
var stockItemIds = stockItems.Select(s => s.Id).ToHashSet();
|
|
||||||
offerings = offerings.Where(o => stockItemIds.Contains(o.StockItemId)).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (stockItemId.HasValue)
|
|
||||||
{
|
|
||||||
offerings = await _api.GetOfferingsForStockItemAsync(stockItemId.Value);
|
|
||||||
}
|
|
||||||
else if (materialId.HasValue)
|
|
||||||
{
|
|
||||||
// Get stock items for this material, then aggregate offerings
|
|
||||||
var stockItems = await _api.GetStockItemsAsync(materialId);
|
|
||||||
var allOfferings = new List<ApiOfferingDto>();
|
|
||||||
foreach (var si in stockItems)
|
|
||||||
{
|
|
||||||
var siOfferings = await _api.GetOfferingsForStockItemAsync(si.Id);
|
|
||||||
allOfferings.AddRange(siOfferings);
|
|
||||||
}
|
|
||||||
offerings = allOfferings;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No filter - get all suppliers then aggregate
|
|
||||||
var suppliers = await _api.GetSuppliersAsync();
|
|
||||||
var allOfferings = new List<ApiOfferingDto>();
|
|
||||||
foreach (var s in suppliers)
|
|
||||||
{
|
|
||||||
var sOfferings = await _api.GetOfferingsForSupplierAsync(s.Id);
|
|
||||||
allOfferings.AddRange(sOfferings);
|
|
||||||
}
|
|
||||||
offerings = allOfferings;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SupplierOfferingListResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Offerings = offerings.Select(o => new SupplierOfferingDto
|
|
||||||
{
|
|
||||||
Id = o.Id,
|
|
||||||
SupplierId = o.SupplierId,
|
|
||||||
SupplierName = o.SupplierName ?? string.Empty,
|
|
||||||
StockItemId = o.StockItemId,
|
|
||||||
MaterialName = o.MaterialName ?? string.Empty,
|
|
||||||
LengthFormatted = o.LengthFormatted ?? string.Empty,
|
|
||||||
PartNumber = o.PartNumber,
|
|
||||||
SupplierDescription = o.SupplierDescription,
|
|
||||||
Price = o.Price,
|
|
||||||
Notes = o.Notes
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool(Name = "add_supplier_offering"), Description("Adds a supplier offering - links a supplier to a stock item with their part number and pricing.")]
|
|
||||||
public async Task<SupplierOfferingResult> AddSupplierOffering(
|
|
||||||
[Description("Supplier ID (use list_suppliers to find)")]
|
|
||||||
int supplierId,
|
|
||||||
[Description("Stock item ID (use list_stock_items to find)")]
|
|
||||||
int stockItemId,
|
|
||||||
[Description("Supplier's part number")]
|
|
||||||
string? partNumber = null,
|
|
||||||
[Description("Supplier's description of the item")]
|
|
||||||
string? supplierDescription = null,
|
|
||||||
[Description("Price per unit")]
|
|
||||||
decimal? price = null,
|
|
||||||
[Description("Notes")]
|
|
||||||
string? notes = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var offering = await _api.CreateOfferingAsync(supplierId, stockItemId, partNumber, supplierDescription, price, notes);
|
|
||||||
|
|
||||||
if (offering == null)
|
|
||||||
return new SupplierOfferingResult { Success = false, Error = "Failed to create offering" };
|
|
||||||
|
|
||||||
return new SupplierOfferingResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Offering = new SupplierOfferingDto
|
|
||||||
{
|
|
||||||
Id = offering.Id,
|
|
||||||
SupplierId = offering.SupplierId,
|
|
||||||
SupplierName = offering.SupplierName ?? string.Empty,
|
|
||||||
StockItemId = offering.StockItemId,
|
|
||||||
MaterialName = offering.MaterialName ?? string.Empty,
|
|
||||||
LengthFormatted = offering.LengthFormatted ?? string.Empty,
|
|
||||||
PartNumber = offering.PartNumber,
|
|
||||||
SupplierDescription = offering.SupplierDescription,
|
|
||||||
Price = offering.Price,
|
|
||||||
Notes = offering.Notes
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (ApiConflictException ex)
|
|
||||||
{
|
|
||||||
return new SupplierOfferingResult { Success = false, Error = ex.Message };
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
return new SupplierOfferingResult { Success = false, Error = ex.Message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool(Name = "add_stock_with_offering"), Description("Convenience method: adds a material (if needed), stock item (if needed), and supplier offering all at once.")]
|
|
||||||
public async Task<AddStockWithOfferingResult> AddStockWithOffering(
|
|
||||||
[Description("Supplier ID (use list_suppliers or add_supplier first)")]
|
|
||||||
int supplierId,
|
|
||||||
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
|
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
|
||||||
string shape,
|
string shape,
|
||||||
[Description("Material size (e.g., '2 x 2 x 1/4')")]
|
[Description("Material size (e.g., '2 x 2 x 1/4')")]
|
||||||
string size,
|
string size,
|
||||||
[Description("Stock length (e.g., '20'', '240')")]
|
[Description("Stock length (e.g., '20'', '240')")]
|
||||||
string length,
|
string length,
|
||||||
|
[Description("Quantity on hand (default 0)")]
|
||||||
|
int quantityOnHand = 0,
|
||||||
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
|
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
|
||||||
string type = "Steel",
|
string type = "Steel",
|
||||||
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
|
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
|
||||||
string? grade = null,
|
string? grade = null)
|
||||||
[Description("Supplier's part number")]
|
|
||||||
string? partNumber = null,
|
|
||||||
[Description("Supplier's description")]
|
|
||||||
string? supplierDescription = null,
|
|
||||||
[Description("Price per unit")]
|
|
||||||
decimal? price = null)
|
|
||||||
{
|
{
|
||||||
// Parse length for formatted display
|
// Parse length for formatted display
|
||||||
double lengthInches;
|
double lengthInches;
|
||||||
@@ -441,7 +262,7 @@ public class InventoryTools
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return new AddStockWithOfferingResult
|
return new AddStockResult
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Error = $"Could not parse length: {length}"
|
Error = $"Could not parse length: {length}"
|
||||||
@@ -478,12 +299,12 @@ public class InventoryTools
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
return new AddStockWithOfferingResult { Success = false, Error = $"Failed to create material: {ex.Message}" };
|
return new AddStockResult { Success = false, Error = $"Failed to create material: {ex.Message}" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (material == null)
|
if (material == null)
|
||||||
return new AddStockWithOfferingResult { Success = false, Error = "Failed to find or create material" };
|
return new AddStockResult { Success = false, Error = "Failed to find or create material" };
|
||||||
|
|
||||||
// Step 2: Find or create stock item
|
// Step 2: Find or create stock item
|
||||||
bool stockItemCreated = false;
|
bool stockItemCreated = false;
|
||||||
@@ -494,7 +315,7 @@ public class InventoryTools
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, 0, null);
|
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, quantityOnHand, null);
|
||||||
stockItemCreated = true;
|
stockItemCreated = true;
|
||||||
}
|
}
|
||||||
catch (ApiConflictException)
|
catch (ApiConflictException)
|
||||||
@@ -505,7 +326,7 @@ public class InventoryTools
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
return new AddStockWithOfferingResult
|
return new AddStockResult
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Error = $"Failed to create stock item: {ex.Message}",
|
Error = $"Failed to create stock item: {ex.Message}",
|
||||||
@@ -515,53 +336,24 @@ public class InventoryTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stockItem == null)
|
if (stockItem == null)
|
||||||
return new AddStockWithOfferingResult
|
return new AddStockResult
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Error = "Failed to find or create stock item",
|
Error = "Failed to find or create stock item",
|
||||||
MaterialCreated = materialCreated
|
MaterialCreated = materialCreated
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 3: Create offering
|
return new AddStockResult
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var offering = await _api.CreateOfferingAsync(supplierId, stockItem.Id, partNumber, supplierDescription, price, null);
|
Success = true,
|
||||||
|
MaterialId = material.Id,
|
||||||
return new AddStockWithOfferingResult
|
MaterialName = $"{material.Shape} - {material.Size}",
|
||||||
{
|
MaterialCreated = materialCreated,
|
||||||
Success = true,
|
StockItemId = stockItem.Id,
|
||||||
MaterialId = material.Id,
|
StockItemCreated = stockItemCreated,
|
||||||
MaterialName = $"{material.Shape} - {material.Size}",
|
LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
|
||||||
MaterialCreated = materialCreated,
|
QuantityOnHand = stockItem.QuantityOnHand
|
||||||
StockItemId = stockItem.Id,
|
};
|
||||||
StockItemCreated = stockItemCreated,
|
|
||||||
LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
|
|
||||||
OfferingId = offering?.Id ?? 0,
|
|
||||||
PartNumber = partNumber,
|
|
||||||
SupplierDescription = supplierDescription,
|
|
||||||
Price = price
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (ApiConflictException)
|
|
||||||
{
|
|
||||||
return new AddStockWithOfferingResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Error = $"Offering for this supplier and stock item already exists",
|
|
||||||
MaterialCreated = materialCreated,
|
|
||||||
StockItemCreated = stockItemCreated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
return new AddStockWithOfferingResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Error = $"Failed to create offering: {ex.Message}",
|
|
||||||
MaterialCreated = materialCreated,
|
|
||||||
StockItemCreated = stockItemCreated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -676,29 +468,6 @@ public class InventoryTools
|
|||||||
|
|
||||||
#region DTOs
|
#region DTOs
|
||||||
|
|
||||||
public class SupplierDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SupplierListResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Error { get; set; }
|
|
||||||
public List<SupplierDto> Suppliers { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SupplierResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Error { get; set; }
|
|
||||||
public SupplierDto? Supplier { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MaterialDimensionsDto
|
public class MaterialDimensionsDto
|
||||||
{
|
{
|
||||||
public double? Diameter { get; set; }
|
public double? Diameter { get; set; }
|
||||||
@@ -769,35 +538,7 @@ public class StockItemResult
|
|||||||
public StockItemDto? StockItem { get; set; }
|
public StockItemDto? StockItem { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SupplierOfferingDto
|
public class AddStockResult
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int SupplierId { get; set; }
|
|
||||||
public string SupplierName { get; set; } = string.Empty;
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public string MaterialName { get; set; } = string.Empty;
|
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SupplierOfferingListResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Error { get; set; }
|
|
||||||
public List<SupplierOfferingDto> Offerings { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SupplierOfferingResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Error { get; set; }
|
|
||||||
public SupplierOfferingDto? Offering { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AddStockWithOfferingResult
|
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public string? Error { get; set; }
|
public string? Error { get; set; }
|
||||||
@@ -807,10 +548,7 @@ public class AddStockWithOfferingResult
|
|||||||
public int StockItemId { get; set; }
|
public int StockItemId { get; set; }
|
||||||
public bool StockItemCreated { get; set; }
|
public bool StockItemCreated { get; set; }
|
||||||
public string LengthFormatted { get; set; } = string.Empty;
|
public string LengthFormatted { get; set; } = string.Empty;
|
||||||
public int OfferingId { get; set; }
|
public int QuantityOnHand { get; set; }
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -28,16 +28,6 @@
|
|||||||
<span class="bi bi-boxes-nav-menu" aria-hidden="true"></span> Stock Items
|
<span class="bi bi-boxes-nav-menu" aria-hidden="true"></span> Stock Items
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</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">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="tools">
|
<NavLink class="nav-link" href="tools">
|
||||||
<span class="bi bi-tools-nav-menu" aria-hidden="true"></span> Cutting 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>
|
<p class="lead">1D Bin Packing Optimization for Material Cutting</p>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<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 h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Jobs</h5>
|
<h5 class="card-title">Jobs</h5>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Materials</h5>
|
<h5 class="card-title">Materials</h5>
|
||||||
@@ -25,16 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">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="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Cutting Tools</h5>
|
<h5 class="card-title">Cutting Tools</h5>
|
||||||
@@ -50,9 +41,9 @@
|
|||||||
<h4>How It Works</h4>
|
<h4>How It Works</h4>
|
||||||
<ol>
|
<ol>
|
||||||
<li><strong>Set up materials</strong> - Define the shapes and sizes of materials you work with</li>
|
<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>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>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>
|
<li><strong>Print report</strong> - Generate a printable cut list to take to the shop</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
@page "/jobs/{Id:int}"
|
@page "/jobs/{Id:int}"
|
||||||
@inject JobService JobService
|
@inject JobService JobService
|
||||||
@inject MaterialService MaterialService
|
@inject MaterialService MaterialService
|
||||||
@inject StockItemService StockItemService
|
|
||||||
@inject CutListPackingService PackingService
|
@inject CutListPackingService PackingService
|
||||||
@inject PurchaseItemService PurchaseItemService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@using CutList.Core
|
@using CutList.Core
|
||||||
@@ -24,7 +22,7 @@
|
|||||||
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-3">
|
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-lock-fill me-2"></i>
|
<i class="bi bi-lock-fill me-2"></i>
|
||||||
<strong>This job is locked</strong> — materials ordered on @job.LockedAt!.Value.ToLocalTime().ToString("g"). Unlock to make changes.
|
<strong>This job is locked</strong> — locked on @job.LockedAt!.Value.ToLocalTime().ToString("g"). Unlock to make changes.
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline-warning btn-sm" @onclick="UnlockJob">
|
<button class="btn btn-outline-warning btn-sm" @onclick="UnlockJob">
|
||||||
<i class="bi bi-unlock"></i> Unlock Job
|
<i class="bi bi-unlock"></i> Unlock Job
|
||||||
@@ -374,8 +372,7 @@ else
|
|||||||
private MultiMaterialPackResult? packResult;
|
private MultiMaterialPackResult? packResult;
|
||||||
private MultiMaterialPackingSummary? summary;
|
private MultiMaterialPackingSummary? summary;
|
||||||
private bool optimizing;
|
private bool optimizing;
|
||||||
private bool addingToOrderList;
|
private bool lockingJob;
|
||||||
private bool addedToOrderList;
|
|
||||||
|
|
||||||
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
||||||
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
|
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
|
||||||
@@ -443,7 +440,6 @@ else
|
|||||||
if (packResult != null)
|
if (packResult != null)
|
||||||
{
|
{
|
||||||
summary = PackingService.GetSummary(packResult);
|
summary = PackingService.GetSummary(packResult);
|
||||||
addedToOrderList = job.IsLocked;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1047,22 +1043,19 @@ else
|
|||||||
<div class="card mb-4 print-purchase-list">
|
<div class="card mb-4 print-purchase-list">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0"><i class="bi bi-cart me-2"></i>Purchase List</h5>
|
<h5 class="mb-0"><i class="bi bi-cart me-2"></i>Purchase List</h5>
|
||||||
@if (summary.TotalToBePurchasedBins > 0)
|
@if (job.IsLocked)
|
||||||
{
|
{
|
||||||
@if (addedToOrderList)
|
<span class="badge bg-success"><i class="bi bi-lock-fill me-1"></i>Job Locked</span>
|
||||||
{
|
}
|
||||||
<span class="badge bg-success"><i class="bi bi-check-lg me-1"></i>Added to orders</span>
|
else
|
||||||
}
|
{
|
||||||
else
|
<button class="btn btn-warning btn-sm" @onclick="LockJob" disabled="@lockingJob">
|
||||||
{
|
@if (lockingJob)
|
||||||
<button class="btn btn-warning btn-sm" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
{
|
||||||
@if (addingToOrderList)
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
{
|
}
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
<i class="bi bi-lock me-1"></i>Lock Job
|
||||||
}
|
</button>
|
||||||
<i class="bi bi-cart-plus me-1"></i>Add to Order List
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -1072,12 +1065,6 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@if (addedToOrderList)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success py-2 mb-3">
|
|
||||||
Items added to order list. <a href="orders">View Orders</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1214,7 +1201,6 @@ else
|
|||||||
|
|
||||||
// Refresh job to get updated OptimizedAt
|
// Refresh job to get updated OptimizedAt
|
||||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
addedToOrderList = job.IsLocked;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1222,55 +1208,17 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddToOrderList()
|
private async Task LockJob()
|
||||||
{
|
{
|
||||||
addingToOrderList = true;
|
lockingJob = true;
|
||||||
try
|
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!.Value,
|
|
||||||
Status = PurchaseItemStatus.Pending
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (purchaseItems.Count > 0)
|
|
||||||
{
|
|
||||||
await PurchaseItemService.CreateBulkAsync(purchaseItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
await JobService.LockAsync(Id!.Value);
|
await JobService.LockAsync(Id!.Value);
|
||||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
addedToOrderList = true;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
addingToOrderList = false;
|
lockingJob = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
@page "/stock/{Id:int}"
|
@page "/stock/{Id:int}"
|
||||||
@inject StockItemService StockItemService
|
@inject StockItemService StockItemService
|
||||||
@inject MaterialService MaterialService
|
@inject MaterialService MaterialService
|
||||||
@inject SupplierService SupplierService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@using CutList.Core.Formatting
|
@using CutList.Core.Formatting
|
||||||
|
|
||||||
@@ -39,14 +38,7 @@ else
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Length</label>
|
<label class="form-label">Length</label>
|
||||||
@if (IsNew)
|
<LengthInput @bind-Value="stockItem.LengthInches" />
|
||||||
{
|
|
||||||
<LengthInput @bind-Value="stockItem.LengthInches" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<input type="text" class="form-control" value="@ArchUnits.FormatFromInches((double)stockItem.LengthInches)" readonly />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -82,7 +74,7 @@ else
|
|||||||
@if (!IsNew)
|
@if (!IsNew)
|
||||||
{
|
{
|
||||||
<div class="col-lg-6 mb-4">
|
<div class="col-lg-6 mb-4">
|
||||||
<div class="card mb-4">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
Inventory
|
Inventory
|
||||||
@@ -112,23 +104,6 @@ else
|
|||||||
<label class="form-label">Notes</label>
|
<label class="form-label">Notes</label>
|
||||||
<InputText class="form-control" @bind-Value="stockNotes" />
|
<InputText class="form-control" @bind-Value="stockNotes" />
|
||||||
</div>
|
</div>
|
||||||
@if (stockTransactionType == "add")
|
|
||||||
{
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Supplier (optional)</label>
|
|
||||||
<select class="form-select" @bind="stockSupplierId">
|
|
||||||
<option value="0">-- Select Supplier --</option>
|
|
||||||
@foreach (var supplier in suppliers)
|
|
||||||
{
|
|
||||||
<option value="@supplier.Id">@supplier.Name</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Unit Price (optional)</label>
|
|
||||||
<input type="number" class="form-control" @bind="stockUnitPrice" step="0.01" min="0" placeholder="0.00" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrEmpty(stockFormErrorMessage))
|
@if (!string.IsNullOrEmpty(stockFormErrorMessage))
|
||||||
{
|
{
|
||||||
@@ -159,8 +134,6 @@ else
|
|||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Qty</th>
|
<th>Qty</th>
|
||||||
<th>Supplier</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -175,8 +148,6 @@ else
|
|||||||
<td class="@(txn.Quantity >= 0 ? "text-success" : "text-danger")">
|
<td class="@(txn.Quantity >= 0 ? "text-success" : "text-danger")">
|
||||||
@(txn.Quantity >= 0 ? "+" : "")@txn.Quantity
|
@(txn.Quantity >= 0 ? "+" : "")@txn.Quantity
|
||||||
</td>
|
</td>
|
||||||
<td>@(txn.Supplier?.Name ?? "-")</td>
|
|
||||||
<td>@(txn.UnitPrice.HasValue ? txn.UnitPrice.Value.ToString("C") : "-")</td>
|
|
||||||
<td>@(txn.Notes ?? "-")</td>
|
<td>@(txn.Notes ?? "-")</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -185,145 +156,35 @@ else
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">Supplier</label>
|
|
||||||
<select class="form-select" @bind="newOffering.SupplierId">
|
|
||||||
<option value="0">-- Select Supplier --</option>
|
|
||||||
@foreach (var supplier in suppliers)
|
|
||||||
{
|
|
||||||
<option value="@supplier.Id">@supplier.Name</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 supplier offerings configured yet.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Supplier</th>
|
|
||||||
<th>Part #</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th style="width: 100px;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var offering in offerings)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@offering.Supplier.Name</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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteOfferingDialog"
|
|
||||||
Title="Delete Offering"
|
|
||||||
Message="@deleteOfferingMessage"
|
|
||||||
ConfirmText="Delete"
|
|
||||||
OnConfirm="DeleteOfferingConfirmed" />
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int? Id { get; set; }
|
public int? Id { get; set; }
|
||||||
|
|
||||||
private StockItem stockItem = new();
|
private StockItem stockItem = new();
|
||||||
private List<Material> materials = new();
|
private List<Material> materials = new();
|
||||||
private List<Supplier> suppliers = new();
|
|
||||||
private List<SupplierOffering> offerings = new();
|
|
||||||
private List<StockTransaction> transactions = new();
|
private List<StockTransaction> transactions = new();
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
private bool saving;
|
private bool saving;
|
||||||
private bool savingOffering;
|
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private string? offeringErrorMessage;
|
|
||||||
|
|
||||||
private bool showOfferingForm;
|
|
||||||
private SupplierOffering newOffering = new();
|
|
||||||
private SupplierOffering? editingOffering;
|
|
||||||
|
|
||||||
// Stock transaction form
|
// Stock transaction form
|
||||||
private bool showStockForm;
|
private bool showStockForm;
|
||||||
private bool savingStockTransaction;
|
private bool savingStockTransaction;
|
||||||
private string stockTransactionType = "add";
|
private string stockTransactionType = "add";
|
||||||
private int stockQuantity;
|
private int stockQuantity;
|
||||||
private int stockSupplierId;
|
|
||||||
private decimal? stockUnitPrice;
|
|
||||||
private string? stockNotes;
|
private string? stockNotes;
|
||||||
private string? stockFormErrorMessage;
|
private string? stockFormErrorMessage;
|
||||||
|
|
||||||
private ConfirmDialog deleteOfferingDialog = null!;
|
|
||||||
private SupplierOffering? offeringToDelete;
|
|
||||||
private string deleteOfferingMessage = "";
|
|
||||||
|
|
||||||
private bool IsNew => !Id.HasValue;
|
private bool IsNew => !Id.HasValue;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
materials = await MaterialService.GetAllAsync();
|
materials = await MaterialService.GetAllAsync();
|
||||||
suppliers = await SupplierService.GetAllAsync();
|
|
||||||
|
|
||||||
if (Id.HasValue)
|
if (Id.HasValue)
|
||||||
{
|
{
|
||||||
@@ -334,7 +195,6 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stockItem = existing;
|
stockItem = existing;
|
||||||
offerings = existing.SupplierOfferings.Where(o => o.IsActive).ToList();
|
|
||||||
transactions = await StockItemService.GetTransactionHistoryAsync(Id.Value, 20);
|
transactions = await StockItemService.GetTransactionHistoryAsync(Id.Value, 20);
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -354,8 +214,6 @@ else
|
|||||||
{
|
{
|
||||||
stockTransactionType = "add";
|
stockTransactionType = "add";
|
||||||
stockQuantity = 0;
|
stockQuantity = 0;
|
||||||
stockSupplierId = 0;
|
|
||||||
stockUnitPrice = null;
|
|
||||||
stockNotes = null;
|
stockNotes = null;
|
||||||
stockFormErrorMessage = null;
|
stockFormErrorMessage = null;
|
||||||
showStockForm = true;
|
showStockForm = true;
|
||||||
@@ -389,12 +247,7 @@ else
|
|||||||
switch (stockTransactionType)
|
switch (stockTransactionType)
|
||||||
{
|
{
|
||||||
case "add":
|
case "add":
|
||||||
await StockItemService.AddStockAsync(
|
await StockItemService.AddStockAsync(Id!.Value, stockQuantity, stockNotes);
|
||||||
Id!.Value,
|
|
||||||
stockQuantity,
|
|
||||||
stockSupplierId > 0 ? stockSupplierId : null,
|
|
||||||
stockUnitPrice,
|
|
||||||
stockNotes);
|
|
||||||
break;
|
break;
|
||||||
case "adjust":
|
case "adjust":
|
||||||
await StockItemService.AdjustStockAsync(Id!.Value, stockQuantity, stockNotes);
|
await StockItemService.AdjustStockAsync(Id!.Value, stockQuantity, stockNotes);
|
||||||
@@ -449,14 +302,21 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsNew)
|
try
|
||||||
{
|
{
|
||||||
var created = await StockItemService.CreateAsync(stockItem);
|
if (IsNew)
|
||||||
Navigation.NavigateTo($"stock/{created.Id}");
|
{
|
||||||
|
var created = await StockItemService.CreateAsync(stockItem);
|
||||||
|
Navigation.NavigateTo($"stock/{created.Id}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await StockItemService.UpdateAsync(stockItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
|
||||||
{
|
{
|
||||||
await StockItemService.UpdateAsync(stockItem);
|
errorMessage = "A stock item with this material and length already exists (it may have been previously deleted).";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -464,108 +324,4 @@ else
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowAddOfferingForm()
|
|
||||||
{
|
|
||||||
editingOffering = null;
|
|
||||||
newOffering = new SupplierOffering { StockItemId = Id!.Value };
|
|
||||||
showOfferingForm = true;
|
|
||||||
offeringErrorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EditOffering(SupplierOffering offering)
|
|
||||||
{
|
|
||||||
editingOffering = offering;
|
|
||||||
newOffering = new SupplierOffering
|
|
||||||
{
|
|
||||||
Id = offering.Id,
|
|
||||||
StockItemId = offering.StockItemId,
|
|
||||||
SupplierId = offering.SupplierId,
|
|
||||||
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.SupplierId == 0)
|
|
||||||
{
|
|
||||||
offeringErrorMessage = "Please select a supplier";
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the stock item to get updated offerings
|
|
||||||
var updated = await StockItemService.GetByIdAsync(Id!.Value);
|
|
||||||
if (updated != null)
|
|
||||||
{
|
|
||||||
stockItem = updated;
|
|
||||||
offerings = updated.SupplierOfferings.Where(o => o.IsActive).ToList();
|
|
||||||
}
|
|
||||||
showOfferingForm = false;
|
|
||||||
editingOffering = null;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
savingOffering = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfirmDeleteOffering(SupplierOffering offering)
|
|
||||||
{
|
|
||||||
offeringToDelete = offering;
|
|
||||||
deleteOfferingMessage = $"Are you sure you want to delete the offering from {offering.Supplier.Name}?";
|
|
||||||
deleteOfferingDialog.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteOfferingConfirmed()
|
|
||||||
{
|
|
||||||
if (offeringToDelete != null)
|
|
||||||
{
|
|
||||||
await SupplierService.DeleteOfferingAsync(offeringToDelete.Id);
|
|
||||||
|
|
||||||
// Refresh the stock item to get updated offerings
|
|
||||||
var updated = await StockItemService.GetByIdAsync(Id!.Value);
|
|
||||||
if (updated != null)
|
|
||||||
{
|
|
||||||
stockItem = updated;
|
|
||||||
offerings = updated.SupplierOfferings.Where(o => o.IsActive).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,12 +11,10 @@ namespace CutList.Web.Controllers;
|
|||||||
public class StockItemsController : ControllerBase
|
public class StockItemsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly StockItemService _stockItemService;
|
private readonly StockItemService _stockItemService;
|
||||||
private readonly SupplierService _supplierService;
|
|
||||||
|
|
||||||
public StockItemsController(StockItemService stockItemService, SupplierService supplierService)
|
public StockItemsController(StockItemService stockItemService)
|
||||||
{
|
{
|
||||||
_stockItemService = stockItemService;
|
_stockItemService = stockItemService;
|
||||||
_supplierService = supplierService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -120,34 +118,6 @@ public class StockItemsController : ControllerBase
|
|||||||
return Ok(items.Select(MapToDto).ToList());
|
return Ok(items.Select(MapToDto).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/offerings")]
|
|
||||||
public async Task<ActionResult<List<OfferingDto>>> GetOfferings(int id)
|
|
||||||
{
|
|
||||||
var item = await _stockItemService.GetByIdAsync(id);
|
|
||||||
if (item == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var offerings = await _supplierService.GetOfferingsForStockItemAsync(id);
|
|
||||||
return Ok(offerings.Select(MapOfferingToDto).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/pricing")]
|
|
||||||
public async Task<ActionResult<StockPricingDto>> GetPricing(int id)
|
|
||||||
{
|
|
||||||
var item = await _stockItemService.GetByIdAsync(id);
|
|
||||||
if (item == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var avgCost = await _stockItemService.GetAverageCostAsync(id);
|
|
||||||
var lastPrice = await _stockItemService.GetLastPurchasePriceAsync(id);
|
|
||||||
|
|
||||||
return Ok(new StockPricingDto
|
|
||||||
{
|
|
||||||
AverageCost = avgCost,
|
|
||||||
LastPurchasePrice = lastPrice
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/transactions")]
|
[HttpGet("{id}/transactions")]
|
||||||
public async Task<ActionResult<List<StockTransactionDto>>> GetTransactions(int id, [FromQuery] int? limit = null)
|
public async Task<ActionResult<List<StockTransactionDto>>> GetTransactions(int id, [FromQuery] int? limit = null)
|
||||||
{
|
{
|
||||||
@@ -164,7 +134,7 @@ public class StockItemsController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var transaction = await _stockItemService.AddStockAsync(id, dto.Quantity, dto.SupplierId, dto.UnitPrice, dto.Notes);
|
var transaction = await _stockItemService.AddStockAsync(id, dto.Quantity, dto.Notes);
|
||||||
return Ok(MapTransactionToDto(transaction));
|
return Ok(MapTransactionToDto(transaction));
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
@@ -250,23 +220,7 @@ public class StockItemsController : ControllerBase
|
|||||||
Type = t.Type.ToString(),
|
Type = t.Type.ToString(),
|
||||||
JobId = t.JobId,
|
JobId = t.JobId,
|
||||||
JobNumber = t.Job?.JobNumber,
|
JobNumber = t.Job?.JobNumber,
|
||||||
SupplierId = t.SupplierId,
|
|
||||||
SupplierName = t.Supplier?.Name,
|
|
||||||
UnitPrice = t.UnitPrice,
|
|
||||||
Notes = t.Notes,
|
Notes = t.Notes,
|
||||||
CreatedAt = t.CreatedAt
|
CreatedAt = t.CreatedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
private static OfferingDto MapOfferingToDto(SupplierOffering o) => new()
|
|
||||||
{
|
|
||||||
Id = o.Id,
|
|
||||||
SupplierId = o.SupplierId,
|
|
||||||
SupplierName = o.Supplier?.Name,
|
|
||||||
StockItemId = o.StockItemId,
|
|
||||||
PartNumber = o.PartNumber,
|
|
||||||
SupplierDescription = o.SupplierDescription,
|
|
||||||
Price = o.Price,
|
|
||||||
Notes = o.Notes,
|
|
||||||
IsActive = o.IsActive
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
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/[controller]")]
|
|
||||||
public class SuppliersController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly SupplierService _supplierService;
|
|
||||||
|
|
||||||
public SuppliersController(SupplierService supplierService)
|
|
||||||
{
|
|
||||||
_supplierService = supplierService;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ActionResult<List<SupplierDto>>> GetAll([FromQuery] bool includeInactive = false)
|
|
||||||
{
|
|
||||||
var suppliers = await _supplierService.GetAllAsync(includeInactive);
|
|
||||||
return Ok(suppliers.Select(MapToDto).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task<ActionResult<SupplierDto>> GetById(int id)
|
|
||||||
{
|
|
||||||
var supplier = await _supplierService.GetByIdAsync(id);
|
|
||||||
if (supplier == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
return Ok(MapToDto(supplier));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<ActionResult<SupplierDto>> Create(CreateSupplierDto dto)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
|
||||||
return BadRequest("Name is required");
|
|
||||||
|
|
||||||
var supplier = new Supplier
|
|
||||||
{
|
|
||||||
Name = dto.Name,
|
|
||||||
ContactInfo = dto.ContactInfo,
|
|
||||||
Notes = dto.Notes
|
|
||||||
};
|
|
||||||
|
|
||||||
await _supplierService.CreateAsync(supplier);
|
|
||||||
return CreatedAtAction(nameof(GetById), new { id = supplier.Id }, MapToDto(supplier));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task<ActionResult<SupplierDto>> Update(int id, UpdateSupplierDto dto)
|
|
||||||
{
|
|
||||||
var supplier = await _supplierService.GetByIdAsync(id);
|
|
||||||
if (supplier == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
if (dto.Name != null) supplier.Name = dto.Name;
|
|
||||||
if (dto.ContactInfo != null) supplier.ContactInfo = dto.ContactInfo;
|
|
||||||
if (dto.Notes != null) supplier.Notes = dto.Notes;
|
|
||||||
|
|
||||||
await _supplierService.UpdateAsync(supplier);
|
|
||||||
return Ok(MapToDto(supplier));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<IActionResult> Delete(int id)
|
|
||||||
{
|
|
||||||
var supplier = await _supplierService.GetByIdAsync(id);
|
|
||||||
if (supplier == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
await _supplierService.DeleteAsync(id);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Offerings ---
|
|
||||||
|
|
||||||
[HttpGet("{id}/offerings")]
|
|
||||||
public async Task<ActionResult<List<OfferingDto>>> GetOfferings(int id)
|
|
||||||
{
|
|
||||||
var supplier = await _supplierService.GetByIdAsync(id);
|
|
||||||
if (supplier == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var offerings = await _supplierService.GetOfferingsForSupplierAsync(id);
|
|
||||||
return Ok(offerings.Select(MapOfferingToDto).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/offerings")]
|
|
||||||
public async Task<ActionResult<OfferingDto>> CreateOffering(int id, CreateOfferingDto dto)
|
|
||||||
{
|
|
||||||
var supplier = await _supplierService.GetByIdAsync(id);
|
|
||||||
if (supplier == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var exists = await _supplierService.OfferingExistsAsync(id, dto.StockItemId);
|
|
||||||
if (exists)
|
|
||||||
return Conflict("An offering for this supplier and stock item already exists");
|
|
||||||
|
|
||||||
var offering = new SupplierOffering
|
|
||||||
{
|
|
||||||
SupplierId = id,
|
|
||||||
StockItemId = dto.StockItemId,
|
|
||||||
PartNumber = dto.PartNumber,
|
|
||||||
SupplierDescription = dto.SupplierDescription,
|
|
||||||
Price = dto.Price,
|
|
||||||
Notes = dto.Notes
|
|
||||||
};
|
|
||||||
|
|
||||||
await _supplierService.AddOfferingAsync(offering);
|
|
||||||
|
|
||||||
// Reload with includes
|
|
||||||
var created = await _supplierService.GetOfferingByIdAsync(offering.Id);
|
|
||||||
return CreatedAtAction(nameof(GetOfferings), new { id }, MapOfferingToDto(created!));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{supplierId}/offerings/{offeringId}")]
|
|
||||||
public async Task<ActionResult<OfferingDto>> UpdateOffering(int supplierId, int offeringId, UpdateOfferingDto dto)
|
|
||||||
{
|
|
||||||
var offering = await _supplierService.GetOfferingByIdAsync(offeringId);
|
|
||||||
if (offering == null || offering.SupplierId != supplierId)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
if (dto.PartNumber != null) offering.PartNumber = dto.PartNumber;
|
|
||||||
if (dto.SupplierDescription != null) offering.SupplierDescription = dto.SupplierDescription;
|
|
||||||
if (dto.Price.HasValue) offering.Price = dto.Price;
|
|
||||||
if (dto.Notes != null) offering.Notes = dto.Notes;
|
|
||||||
|
|
||||||
await _supplierService.UpdateOfferingAsync(offering);
|
|
||||||
return Ok(MapOfferingToDto(offering));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{supplierId}/offerings/{offeringId}")]
|
|
||||||
public async Task<IActionResult> DeleteOffering(int supplierId, int offeringId)
|
|
||||||
{
|
|
||||||
var offering = await _supplierService.GetOfferingByIdAsync(offeringId);
|
|
||||||
if (offering == null || offering.SupplierId != supplierId)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
await _supplierService.DeleteOfferingAsync(offeringId);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SupplierDto MapToDto(Supplier s) => new()
|
|
||||||
{
|
|
||||||
Id = s.Id,
|
|
||||||
Name = s.Name,
|
|
||||||
ContactInfo = s.ContactInfo,
|
|
||||||
Notes = s.Notes,
|
|
||||||
IsActive = s.IsActive
|
|
||||||
};
|
|
||||||
|
|
||||||
private static OfferingDto MapOfferingToDto(SupplierOffering o) => new()
|
|
||||||
{
|
|
||||||
Id = o.Id,
|
|
||||||
SupplierId = o.SupplierId,
|
|
||||||
SupplierName = o.Supplier?.Name,
|
|
||||||
StockItemId = o.StockItemId,
|
|
||||||
MaterialName = o.StockItem?.Material?.DisplayName,
|
|
||||||
LengthInches = o.StockItem?.LengthInches,
|
|
||||||
LengthFormatted = o.StockItem != null ? ArchUnits.FormatFromInches((double)o.StockItem.LengthInches) : null,
|
|
||||||
PartNumber = o.PartNumber,
|
|
||||||
SupplierDescription = o.SupplierDescription,
|
|
||||||
Price = o.Price,
|
|
||||||
Notes = o.Notes,
|
|
||||||
IsActive = o.IsActive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,18 +3,10 @@ namespace CutList.Web.DTOs;
|
|||||||
public class CatalogData
|
public class CatalogData
|
||||||
{
|
{
|
||||||
public DateTime ExportedAt { get; set; }
|
public DateTime ExportedAt { get; set; }
|
||||||
public List<CatalogSupplierDto> Suppliers { get; set; } = [];
|
|
||||||
public List<CatalogCuttingToolDto> CuttingTools { get; set; } = [];
|
public List<CatalogCuttingToolDto> CuttingTools { get; set; } = [];
|
||||||
public CatalogMaterialsDto Materials { get; set; } = new();
|
public CatalogMaterialsDto Materials { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CatalogSupplierDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CatalogCuttingToolDto
|
public class CatalogCuttingToolDto
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
@@ -113,30 +105,16 @@ public class CatalogStockItemDto
|
|||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public int QuantityOnHand { get; set; }
|
public int QuantityOnHand { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public List<CatalogSupplierOfferingDto> SupplierOfferings { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CatalogSupplierOfferingDto
|
|
||||||
{
|
|
||||||
public string SupplierName { get; set; } = "";
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImportResultDto
|
public class ImportResultDto
|
||||||
{
|
{
|
||||||
public int SuppliersCreated { get; set; }
|
|
||||||
public int SuppliersUpdated { get; set; }
|
|
||||||
public int CuttingToolsCreated { get; set; }
|
public int CuttingToolsCreated { get; set; }
|
||||||
public int CuttingToolsUpdated { get; set; }
|
public int CuttingToolsUpdated { get; set; }
|
||||||
public int MaterialsCreated { get; set; }
|
public int MaterialsCreated { get; set; }
|
||||||
public int MaterialsUpdated { get; set; }
|
public int MaterialsUpdated { get; set; }
|
||||||
public int StockItemsCreated { get; set; }
|
public int StockItemsCreated { get; set; }
|
||||||
public int StockItemsUpdated { get; set; }
|
public int StockItemsUpdated { get; set; }
|
||||||
public int OfferingsCreated { get; set; }
|
|
||||||
public int OfferingsUpdated { get; set; }
|
|
||||||
public List<string> Errors { get; set; } = [];
|
public List<string> Errors { get; set; } = [];
|
||||||
public List<string> Warnings { get; set; } = [];
|
public List<string> Warnings { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ public class StockTransactionDto
|
|||||||
public string Type { get; set; } = string.Empty;
|
public string Type { get; set; } = string.Empty;
|
||||||
public int? JobId { get; set; }
|
public int? JobId { get; set; }
|
||||||
public string? JobNumber { get; set; }
|
public string? JobNumber { get; set; }
|
||||||
public int? SupplierId { get; set; }
|
|
||||||
public string? SupplierName { get; set; }
|
|
||||||
public decimal? UnitPrice { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -47,8 +44,6 @@ public class StockTransactionDto
|
|||||||
public class AddStockDto
|
public class AddStockDto
|
||||||
{
|
{
|
||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public int? SupplierId { get; set; }
|
|
||||||
public decimal? UnitPrice { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +65,3 @@ public class ScrapStockDto
|
|||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StockPricingDto
|
|
||||||
{
|
|
||||||
public decimal? AverageCost { get; set; }
|
|
||||||
public decimal? LastPurchasePrice { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
namespace CutList.Web.DTOs;
|
|
||||||
|
|
||||||
public class SupplierDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CreateSupplierDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateSupplierDto
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OfferingDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int SupplierId { get; set; }
|
|
||||||
public string? SupplierName { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public string? MaterialName { get; set; }
|
|
||||||
public decimal? LengthInches { get; set; }
|
|
||||||
public string? LengthFormatted { get; set; }
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CreateOfferingDto
|
|
||||||
{
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateOfferingDto
|
|
||||||
{
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
@@ -12,15 +12,12 @@ public class ApplicationDbContext : DbContext
|
|||||||
|
|
||||||
public DbSet<Material> Materials => Set<Material>();
|
public DbSet<Material> Materials => Set<Material>();
|
||||||
public DbSet<MaterialDimensions> MaterialDimensions => Set<MaterialDimensions>();
|
public DbSet<MaterialDimensions> MaterialDimensions => Set<MaterialDimensions>();
|
||||||
public DbSet<Supplier> Suppliers => Set<Supplier>();
|
|
||||||
public DbSet<StockItem> StockItems => Set<StockItem>();
|
public DbSet<StockItem> StockItems => Set<StockItem>();
|
||||||
public DbSet<SupplierOffering> SupplierOfferings => Set<SupplierOffering>();
|
|
||||||
public DbSet<StockTransaction> StockTransactions => Set<StockTransaction>();
|
public DbSet<StockTransaction> StockTransactions => Set<StockTransaction>();
|
||||||
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
|
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
|
||||||
public DbSet<Job> Jobs => Set<Job>();
|
public DbSet<Job> Jobs => Set<Job>();
|
||||||
public DbSet<JobPart> JobParts => Set<JobPart>();
|
public DbSet<JobPart> JobParts => Set<JobPart>();
|
||||||
public DbSet<JobStock> JobStocks => Set<JobStock>();
|
public DbSet<JobStock> JobStocks => Set<JobStock>();
|
||||||
public DbSet<PurchaseItem> PurchaseItems => Set<PurchaseItem>();
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -143,15 +140,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
entity.HasIndex(e => e.NominalSize);
|
entity.HasIndex(e => e.NominalSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Supplier
|
|
||||||
modelBuilder.Entity<Supplier>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
|
|
||||||
entity.Property(e => e.ContactInfo).HasMaxLength(500);
|
|
||||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
});
|
|
||||||
|
|
||||||
// StockItem
|
// StockItem
|
||||||
modelBuilder.Entity<StockItem>(entity =>
|
modelBuilder.Entity<StockItem>(entity =>
|
||||||
{
|
{
|
||||||
@@ -174,7 +162,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.Property(e => e.Notes).HasMaxLength(500);
|
entity.Property(e => e.Notes).HasMaxLength(500);
|
||||||
entity.Property(e => e.UnitPrice).HasPrecision(10, 2);
|
|
||||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
entity.HasOne(e => e.StockItem)
|
entity.HasOne(e => e.StockItem)
|
||||||
@@ -186,33 +173,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(e => e.JobId)
|
.HasForeignKey(e => e.JobId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
entity.HasOne(e => e.Supplier)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(e => e.SupplierId)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
// SupplierOffering
|
|
||||||
modelBuilder.Entity<SupplierOffering>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.PartNumber).HasMaxLength(100);
|
|
||||||
entity.Property(e => e.SupplierDescription).HasMaxLength(255);
|
|
||||||
entity.Property(e => e.Price).HasPrecision(10, 2);
|
|
||||||
entity.Property(e => e.Notes).HasMaxLength(255);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.StockItem)
|
|
||||||
.WithMany(s => s.SupplierOfferings)
|
|
||||||
.HasForeignKey(e => e.StockItemId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.Supplier)
|
|
||||||
.WithMany(s => s.Offerings)
|
|
||||||
.HasForeignKey(e => e.SupplierId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasIndex(e => new { e.SupplierId, e.StockItemId }).IsUnique();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// CuttingTool
|
// CuttingTool
|
||||||
@@ -282,34 +242,6 @@ public class ApplicationDbContext : DbContext
|
|||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PurchaseItem
|
|
||||||
modelBuilder.Entity<PurchaseItem>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.Notes).HasMaxLength(500);
|
|
||||||
entity.Property(e => e.Status)
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasConversion(
|
|
||||||
v => v.ToString(),
|
|
||||||
v => Enum.Parse<PurchaseItemStatus>(v));
|
|
||||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
|
|
||||||
entity.HasOne(e => e.StockItem)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(e => e.StockItemId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.Supplier)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(e => e.SupplierId)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
entity.HasOne(e => e.Job)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(e => e.JobId)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Seed default cutting tools
|
// Seed default cutting tools
|
||||||
modelBuilder.Entity<CuttingTool>().HasData(
|
modelBuilder.Entity<CuttingTool>().HasData(
|
||||||
new CuttingTool { Id = 1, Name = "Bandsaw", KerfInches = 0.0625m, IsDefault = true, IsActive = true },
|
new CuttingTool { Id = 1, Name = "Bandsaw", KerfInches = 0.0625m, IsDefault = true, IsActive = true },
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public enum PurchaseItemStatus
|
|
||||||
{
|
|
||||||
Pending,
|
|
||||||
Ordered,
|
|
||||||
Received
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PurchaseItem
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public int? SupplierId { get; set; }
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public int? JobId { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public PurchaseItemStatus Status { get; set; } = PurchaseItemStatus.Pending;
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
|
|
||||||
public StockItem StockItem { get; set; } = null!;
|
|
||||||
public Supplier? Supplier { get; set; }
|
|
||||||
public Job? Job { get; set; }
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,5 @@ public class StockItem
|
|||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
public Material Material { get; set; } = null!;
|
public Material Material { get; set; } = null!;
|
||||||
public ICollection<SupplierOffering> SupplierOfferings { get; set; } = new List<SupplierOffering>();
|
|
||||||
public ICollection<StockTransaction> Transactions { get; set; } = new List<StockTransaction>();
|
public ICollection<StockTransaction> Transactions { get; set; } = new List<StockTransaction>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ public class StockTransaction
|
|||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public StockTransactionType Type { get; set; }
|
public StockTransactionType Type { get; set; }
|
||||||
public int? JobId { get; set; }
|
public int? JobId { get; set; }
|
||||||
public int? SupplierId { get; set; }
|
|
||||||
public decimal? UnitPrice { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public StockItem StockItem { get; set; } = null!;
|
public StockItem StockItem { get; set; } = null!;
|
||||||
public Job? Job { get; set; }
|
public Job? Job { get; set; }
|
||||||
public Supplier? Supplier { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StockTransactionType
|
public enum StockTransactionType
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class Supplier
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
public ICollection<SupplierOffering> Offerings { get; set; } = new List<SupplierOffering>();
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class SupplierOffering
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int StockItemId { get; set; }
|
|
||||||
public int SupplierId { get; set; }
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
|
|
||||||
public StockItem StockItem { get; set; } = null!;
|
|
||||||
public Supplier Supplier { get; set; } = null!;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,684 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260801124832_RemoveVendorData")]
|
||||||
|
partial class RemoveVendorData
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.HasSequence("MaterialDimensionsSequence");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("KerfInches")
|
||||||
|
.HasPrecision(6, 4)
|
||||||
|
.HasColumnType("decimal(6,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CuttingTools");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = true,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Bandsaw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.125m,
|
||||||
|
Name = "Chop Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Cold Cut Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Hacksaw"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Customer")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("CuttingToolId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LockedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("OptimizationResultJson")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("OptimizedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CuttingToolId");
|
||||||
|
|
||||||
|
b.HasIndex("JobNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.ToTable("JobParts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsCustomLength")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.ToTable("JobStocks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Grade")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Shape")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Size")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Materials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable((string)null);
|
||||||
|
|
||||||
|
b.UseTpcMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("QuantityOnHand")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<int?>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.ToTable("StockTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leg1")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leg2")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Leg1");
|
||||||
|
|
||||||
|
b.ToTable("DimAngle", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Flange")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Web")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.ToTable("DimChannel", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.ToTable("DimFlatBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WeightPerFoot")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.ToTable("DimIBeam", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("NominalSize")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Schedule")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("NominalSize");
|
||||||
|
|
||||||
|
b.ToTable("DimPipe", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.ToTable("DimRectangularTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Diameter")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Diameter");
|
||||||
|
|
||||||
|
b.ToTable("DimRoundBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("OuterDiameter")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("OuterDiameter");
|
||||||
|
|
||||||
|
b.ToTable("DimRoundTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.ToTable("DimSquareBar", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.ToTable("DimSquareTube", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||||
|
.WithMany("Jobs")
|
||||||
|
.HasForeignKey("CuttingToolId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("CuttingTool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Parts")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("JobParts")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Stock")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithOne("Dimensions")
|
||||||
|
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockItems")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Parts");
|
||||||
|
|
||||||
|
b.Navigation("Stock");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Dimensions");
|
||||||
|
|
||||||
|
b.Navigation("JobParts");
|
||||||
|
|
||||||
|
b.Navigation("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveVendorData : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_StockTransactions_Suppliers_SupplierId",
|
||||||
|
table: "StockTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PurchaseItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SupplierOfferings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Suppliers");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_StockTransactions_SupplierId",
|
||||||
|
table: "StockTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SupplierId",
|
||||||
|
table: "StockTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UnitPrice",
|
||||||
|
table: "StockTransactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "SupplierId",
|
||||||
|
table: "StockTransactions",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "UnitPrice",
|
||||||
|
table: "StockTransactions",
|
||||||
|
type: "decimal(10,2)",
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Suppliers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ContactInfo = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Suppliers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PurchaseItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
JobId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
StockItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SupplierId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PurchaseItems", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PurchaseItems_Jobs_JobId",
|
||||||
|
column: x => x.JobId,
|
||||||
|
principalTable: "Jobs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PurchaseItems_StockItems_StockItemId",
|
||||||
|
column: x => x.StockItemId,
|
||||||
|
principalTable: "StockItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PurchaseItems_Suppliers_SupplierId",
|
||||||
|
column: x => x.SupplierId,
|
||||||
|
principalTable: "Suppliers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SupplierOfferings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
StockItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SupplierId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
|
||||||
|
PartNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
Price = table.Column<decimal>(type: "decimal(10,2)", precision: 10, scale: 2, nullable: true),
|
||||||
|
SupplierDescription = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SupplierOfferings", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierOfferings_StockItems_StockItemId",
|
||||||
|
column: x => x.StockItemId,
|
||||||
|
principalTable: "StockItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierOfferings_Suppliers_SupplierId",
|
||||||
|
column: x => x.SupplierId,
|
||||||
|
principalTable: "Suppliers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StockTransactions_SupplierId",
|
||||||
|
table: "StockTransactions",
|
||||||
|
column: "SupplierId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PurchaseItems_JobId",
|
||||||
|
table: "PurchaseItems",
|
||||||
|
column: "JobId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PurchaseItems_StockItemId",
|
||||||
|
table: "PurchaseItems",
|
||||||
|
column: "StockItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PurchaseItems_SupplierId",
|
||||||
|
table: "PurchaseItems",
|
||||||
|
column: "SupplierId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierOfferings_StockItemId",
|
||||||
|
table: "SupplierOfferings",
|
||||||
|
column: "StockItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierOfferings_SupplierId_StockItemId",
|
||||||
|
table: "SupplierOfferings",
|
||||||
|
columns: new[] { "SupplierId", "StockItemId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_StockTransactions_Suppliers_SupplierId",
|
||||||
|
table: "StockTransactions",
|
||||||
|
column: "SupplierId",
|
||||||
|
principalTable: "Suppliers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace CutList.Web.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "8.0.11")
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
@@ -294,54 +294,6 @@ namespace CutList.Web.Migrations
|
|||||||
b.UseTpcMappingStrategy();
|
b.UseTpcMappingStrategy();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
|
|
||||||
b.Property<int?>("JobId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("nvarchar(500)");
|
|
||||||
|
|
||||||
b.Property<int>("Quantity")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("nvarchar(20)");
|
|
||||||
|
|
||||||
b.Property<int>("StockItemId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int?>("SupplierId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("StockItemId");
|
|
||||||
|
|
||||||
b.HasIndex("SupplierId");
|
|
||||||
|
|
||||||
b.ToTable("PurchaseItems");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -413,103 +365,18 @@ namespace CutList.Web.Migrations
|
|||||||
b.Property<int>("StockItemId")
|
b.Property<int>("StockItemId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int?>("SupplierId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<decimal?>("UnitPrice")
|
|
||||||
.HasPrecision(10, 2)
|
|
||||||
.HasColumnType("decimal(10,2)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
b.HasIndex("StockItemId");
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
b.HasIndex("SupplierId");
|
|
||||||
|
|
||||||
b.ToTable("StockTransactions");
|
b.ToTable("StockTransactions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ContactInfo")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("nvarchar(500)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasDefaultValueSql("GETUTCDATE()");
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("nvarchar(100)");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Suppliers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("nvarchar(255)");
|
|
||||||
|
|
||||||
b.Property<string>("PartNumber")
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("nvarchar(100)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Price")
|
|
||||||
.HasPrecision(10, 2)
|
|
||||||
.HasColumnType("decimal(10,2)");
|
|
||||||
|
|
||||||
b.Property<int>("StockItemId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("SupplierDescription")
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("nvarchar(255)");
|
|
||||||
|
|
||||||
b.Property<int>("SupplierId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("StockItemId");
|
|
||||||
|
|
||||||
b.HasIndex("SupplierId", "StockItemId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SupplierOfferings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
@@ -754,31 +621,6 @@ namespace CutList.Web.Migrations
|
|||||||
b.Navigation("Material");
|
b.Navigation("Material");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("StockItemId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupplierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
|
||||||
|
|
||||||
b.Navigation("StockItem");
|
|
||||||
|
|
||||||
b.Navigation("Supplier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
@@ -803,35 +645,9 @@ namespace CutList.Web.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupplierId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("Job");
|
b.Navigation("Job");
|
||||||
|
|
||||||
b.Navigation("StockItem");
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
b.Navigation("Supplier");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
|
||||||
.WithMany("SupplierOfferings")
|
|
||||||
.HasForeignKey("StockItemId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
|
||||||
.WithMany("Offerings")
|
|
||||||
.HasForeignKey("SupplierId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("StockItem");
|
|
||||||
|
|
||||||
b.Navigation("Supplier");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
@@ -857,15 +673,8 @@ namespace CutList.Web.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("SupplierOfferings");
|
|
||||||
|
|
||||||
b.Navigation("Transactions");
|
b.Navigation("Transactions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Offerings");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
|
|||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
builder.Services.AddScoped<MaterialService>();
|
builder.Services.AddScoped<MaterialService>();
|
||||||
builder.Services.AddScoped<SupplierService>();
|
|
||||||
builder.Services.AddScoped<StockItemService>();
|
builder.Services.AddScoped<StockItemService>();
|
||||||
builder.Services.AddScoped<JobService>();
|
builder.Services.AddScoped<JobService>();
|
||||||
builder.Services.AddScoped<CutListPackingService>();
|
builder.Services.AddScoped<CutListPackingService>();
|
||||||
builder.Services.AddScoped<ReportService>();
|
builder.Services.AddScoped<ReportService>();
|
||||||
builder.Services.AddScoped<PurchaseItemService>();
|
|
||||||
builder.Services.AddScoped<CatalogService>();
|
builder.Services.AddScoped<CatalogService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ public class CatalogService
|
|||||||
{
|
{
|
||||||
await using var context = _factory.CreateDbContext();
|
await using var context = _factory.CreateDbContext();
|
||||||
|
|
||||||
var suppliers = await context.Suppliers
|
|
||||||
.Where(s => s.IsActive)
|
|
||||||
.OrderBy(s => s.Name)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var cuttingTools = await context.CuttingTools
|
var cuttingTools = await context.CuttingTools
|
||||||
.Where(t => t.IsActive)
|
.Where(t => t.IsActive)
|
||||||
.OrderBy(t => t.Name)
|
.OrderBy(t => t.Name)
|
||||||
@@ -35,7 +29,6 @@ public class CatalogService
|
|||||||
var materials = await context.Materials
|
var materials = await context.Materials
|
||||||
.Include(m => m.Dimensions)
|
.Include(m => m.Dimensions)
|
||||||
.Include(m => m.StockItems.Where(s => s.IsActive))
|
.Include(m => m.StockItems.Where(s => s.IsActive))
|
||||||
.ThenInclude(s => s.SupplierOfferings.Where(o => o.IsActive))
|
|
||||||
.Where(m => m.IsActive)
|
.Where(m => m.IsActive)
|
||||||
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
|
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -48,7 +41,7 @@ public class CatalogService
|
|||||||
{
|
{
|
||||||
foreach (var m in group)
|
foreach (var m in group)
|
||||||
{
|
{
|
||||||
var stockItems = MapStockItems(m, suppliers);
|
var stockItems = MapStockItems(m);
|
||||||
|
|
||||||
switch (m.Shape)
|
switch (m.Shape)
|
||||||
{
|
{
|
||||||
@@ -139,12 +132,6 @@ public class CatalogService
|
|||||||
return new CatalogData
|
return new CatalogData
|
||||||
{
|
{
|
||||||
ExportedAt = DateTime.UtcNow,
|
ExportedAt = DateTime.UtcNow,
|
||||||
Suppliers = suppliers.Select(s => new CatalogSupplierDto
|
|
||||||
{
|
|
||||||
Name = s.Name,
|
|
||||||
ContactInfo = s.ContactInfo,
|
|
||||||
Notes = s.Notes
|
|
||||||
}).ToList(),
|
|
||||||
CuttingTools = cuttingTools.Select(t => new CatalogCuttingToolDto
|
CuttingTools = cuttingTools.Select(t => new CatalogCuttingToolDto
|
||||||
{
|
{
|
||||||
Name = t.Name,
|
Name = t.Name,
|
||||||
@@ -164,14 +151,11 @@ public class CatalogService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Suppliers - upsert by name
|
// 1. Cutting tools - upsert by name
|
||||||
var supplierMap = await ImportSuppliersAsync(context, data.Suppliers, result);
|
|
||||||
|
|
||||||
// 2. Cutting tools - upsert by name
|
|
||||||
await ImportCuttingToolsAsync(context, data.CuttingTools, result);
|
await ImportCuttingToolsAsync(context, data.CuttingTools, result);
|
||||||
|
|
||||||
// 3. Materials + stock items + offerings
|
// 2. Materials + stock items
|
||||||
await ImportAllMaterialsAsync(context, data.Materials, supplierMap, result);
|
await ImportAllMaterialsAsync(context, data.Materials, result);
|
||||||
|
|
||||||
await transaction.CommitAsync();
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
@@ -184,54 +168,6 @@ public class CatalogService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, int>> ImportSuppliersAsync(
|
|
||||||
ApplicationDbContext context, List<CatalogSupplierDto> suppliers, ImportResultDto result)
|
|
||||||
{
|
|
||||||
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var existingSuppliers = await context.Suppliers.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var dto in suppliers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existing = existingSuppliers.FirstOrDefault(
|
|
||||||
s => s.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
existing.ContactInfo = dto.ContactInfo ?? existing.ContactInfo;
|
|
||||||
existing.Notes = dto.Notes ?? existing.Notes;
|
|
||||||
existing.IsActive = true;
|
|
||||||
map[dto.Name] = existing.Id;
|
|
||||||
result.SuppliersUpdated++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var supplier = new Supplier
|
|
||||||
{
|
|
||||||
Name = dto.Name,
|
|
||||||
ContactInfo = dto.ContactInfo,
|
|
||||||
Notes = dto.Notes,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
context.Suppliers.Add(supplier);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
existingSuppliers.Add(supplier);
|
|
||||||
map[dto.Name] = supplier.Id;
|
|
||||||
result.SuppliersCreated++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Errors.Add($"Supplier '{dto.Name}': {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ImportCuttingToolsAsync(
|
private async Task ImportCuttingToolsAsync(
|
||||||
ApplicationDbContext context, List<CatalogCuttingToolDto> tools, ImportResultDto result)
|
ApplicationDbContext context, List<CatalogCuttingToolDto> tools, ImportResultDto result)
|
||||||
{
|
{
|
||||||
@@ -273,68 +209,67 @@ public class CatalogService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task ImportAllMaterialsAsync(
|
private async Task ImportAllMaterialsAsync(
|
||||||
ApplicationDbContext context, CatalogMaterialsDto materials, Dictionary<string, int> supplierMap, ImportResultDto result)
|
ApplicationDbContext context, CatalogMaterialsDto materials, ImportResultDto result)
|
||||||
{
|
{
|
||||||
var existingMaterials = await context.Materials
|
var existingMaterials = await context.Materials
|
||||||
.Include(m => m.Dimensions)
|
.Include(m => m.Dimensions)
|
||||||
.Include(m => m.StockItems)
|
.Include(m => m.StockItems)
|
||||||
.ThenInclude(s => s.SupplierOfferings)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
foreach (var dto in materials.Angles)
|
foreach (var dto in materials.Angles)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.Angle, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.Angle, existingMaterials, result,
|
||||||
() => new AngleDimensions { Leg1 = dto.Leg1, Leg2 = dto.Leg2, Thickness = dto.Thickness },
|
() => new AngleDimensions { Leg1 = dto.Leg1, Leg2 = dto.Leg2, Thickness = dto.Thickness },
|
||||||
dim => { var d = (AngleDimensions)dim; d.Leg1 = dto.Leg1; d.Leg2 = dto.Leg2; d.Thickness = dto.Thickness; });
|
dim => { var d = (AngleDimensions)dim; d.Leg1 = dto.Leg1; d.Leg2 = dto.Leg2; d.Thickness = dto.Thickness; });
|
||||||
|
|
||||||
foreach (var dto in materials.Channels)
|
foreach (var dto in materials.Channels)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.Channel, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.Channel, existingMaterials, result,
|
||||||
() => new ChannelDimensions { Height = dto.Height, Flange = dto.Flange, Web = dto.Web },
|
() => new ChannelDimensions { Height = dto.Height, Flange = dto.Flange, Web = dto.Web },
|
||||||
dim => { var d = (ChannelDimensions)dim; d.Height = dto.Height; d.Flange = dto.Flange; d.Web = dto.Web; });
|
dim => { var d = (ChannelDimensions)dim; d.Height = dto.Height; d.Flange = dto.Flange; d.Web = dto.Web; });
|
||||||
|
|
||||||
foreach (var dto in materials.FlatBars)
|
foreach (var dto in materials.FlatBars)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.FlatBar, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.FlatBar, existingMaterials, result,
|
||||||
() => new FlatBarDimensions { Width = dto.Width, Thickness = dto.Thickness },
|
() => new FlatBarDimensions { Width = dto.Width, Thickness = dto.Thickness },
|
||||||
dim => { var d = (FlatBarDimensions)dim; d.Width = dto.Width; d.Thickness = dto.Thickness; });
|
dim => { var d = (FlatBarDimensions)dim; d.Width = dto.Width; d.Thickness = dto.Thickness; });
|
||||||
|
|
||||||
foreach (var dto in materials.IBeams)
|
foreach (var dto in materials.IBeams)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.IBeam, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.IBeam, existingMaterials, result,
|
||||||
() => new IBeamDimensions { Height = dto.Height, WeightPerFoot = dto.WeightPerFoot },
|
() => new IBeamDimensions { Height = dto.Height, WeightPerFoot = dto.WeightPerFoot },
|
||||||
dim => { var d = (IBeamDimensions)dim; d.Height = dto.Height; d.WeightPerFoot = dto.WeightPerFoot; });
|
dim => { var d = (IBeamDimensions)dim; d.Height = dto.Height; d.WeightPerFoot = dto.WeightPerFoot; });
|
||||||
|
|
||||||
foreach (var dto in materials.Pipes)
|
foreach (var dto in materials.Pipes)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.Pipe, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.Pipe, existingMaterials, result,
|
||||||
() => new PipeDimensions { NominalSize = dto.NominalSize, Wall = dto.Wall, Schedule = dto.Schedule },
|
() => new PipeDimensions { NominalSize = dto.NominalSize, Wall = dto.Wall, Schedule = dto.Schedule },
|
||||||
dim => { var d = (PipeDimensions)dim; d.NominalSize = dto.NominalSize; d.Wall = (decimal?)dto.Wall; d.Schedule = dto.Schedule; });
|
dim => { var d = (PipeDimensions)dim; d.NominalSize = dto.NominalSize; d.Wall = (decimal?)dto.Wall; d.Schedule = dto.Schedule; });
|
||||||
|
|
||||||
foreach (var dto in materials.RectangularTubes)
|
foreach (var dto in materials.RectangularTubes)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.RectangularTube, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.RectangularTube, existingMaterials, result,
|
||||||
() => new RectangularTubeDimensions { Width = dto.Width, Height = dto.Height, Wall = dto.Wall },
|
() => new RectangularTubeDimensions { Width = dto.Width, Height = dto.Height, Wall = dto.Wall },
|
||||||
dim => { var d = (RectangularTubeDimensions)dim; d.Width = dto.Width; d.Height = dto.Height; d.Wall = dto.Wall; });
|
dim => { var d = (RectangularTubeDimensions)dim; d.Width = dto.Width; d.Height = dto.Height; d.Wall = dto.Wall; });
|
||||||
|
|
||||||
foreach (var dto in materials.RoundBars)
|
foreach (var dto in materials.RoundBars)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.RoundBar, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.RoundBar, existingMaterials, result,
|
||||||
() => new RoundBarDimensions { Diameter = dto.Diameter },
|
() => new RoundBarDimensions { Diameter = dto.Diameter },
|
||||||
dim => { var d = (RoundBarDimensions)dim; d.Diameter = dto.Diameter; });
|
dim => { var d = (RoundBarDimensions)dim; d.Diameter = dto.Diameter; });
|
||||||
|
|
||||||
foreach (var dto in materials.RoundTubes)
|
foreach (var dto in materials.RoundTubes)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.RoundTube, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.RoundTube, existingMaterials, result,
|
||||||
() => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter, Wall = dto.Wall },
|
() => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter, Wall = dto.Wall },
|
||||||
dim => { var d = (RoundTubeDimensions)dim; d.OuterDiameter = dto.OuterDiameter; d.Wall = dto.Wall; });
|
dim => { var d = (RoundTubeDimensions)dim; d.OuterDiameter = dto.OuterDiameter; d.Wall = dto.Wall; });
|
||||||
|
|
||||||
foreach (var dto in materials.SquareBars)
|
foreach (var dto in materials.SquareBars)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.SquareBar, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.SquareBar, existingMaterials, result,
|
||||||
() => new SquareBarDimensions { Size = dto.SideLength },
|
() => new SquareBarDimensions { Size = dto.SideLength },
|
||||||
dim => { var d = (SquareBarDimensions)dim; d.Size = dto.SideLength; });
|
dim => { var d = (SquareBarDimensions)dim; d.Size = dto.SideLength; });
|
||||||
|
|
||||||
foreach (var dto in materials.SquareTubes)
|
foreach (var dto in materials.SquareTubes)
|
||||||
await ImportMaterialAsync(context, dto, MaterialShape.SquareTube, existingMaterials, supplierMap, result,
|
await ImportMaterialAsync(context, dto, MaterialShape.SquareTube, existingMaterials, result,
|
||||||
() => new SquareTubeDimensions { Size = dto.SideLength, Wall = dto.Wall },
|
() => new SquareTubeDimensions { Size = dto.SideLength, Wall = dto.Wall },
|
||||||
dim => { var d = (SquareTubeDimensions)dim; d.Size = dto.SideLength; d.Wall = dto.Wall; });
|
dim => { var d = (SquareTubeDimensions)dim; d.Size = dto.SideLength; d.Wall = dto.Wall; });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ImportMaterialAsync(
|
private async Task ImportMaterialAsync(
|
||||||
ApplicationDbContext context, CatalogMaterialBaseDto dto, MaterialShape shape,
|
ApplicationDbContext context, CatalogMaterialBaseDto dto, MaterialShape shape,
|
||||||
List<Material> existingMaterials, Dictionary<string, int> supplierMap,
|
List<Material> existingMaterials,
|
||||||
ImportResultDto result,
|
ImportResultDto result,
|
||||||
Func<MaterialDimensions> createDimensions,
|
Func<MaterialDimensions> createDimensions,
|
||||||
Action<MaterialDimensions> updateDimensions)
|
Action<MaterialDimensions> updateDimensions)
|
||||||
@@ -389,7 +324,7 @@ public class CatalogService
|
|||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
await ImportStockItemsAsync(context, material, dto.StockItems, supplierMap, result);
|
await ImportStockItemsAsync(context, material, dto.StockItems, result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -399,10 +334,9 @@ public class CatalogService
|
|||||||
|
|
||||||
private async Task ImportStockItemsAsync(
|
private async Task ImportStockItemsAsync(
|
||||||
ApplicationDbContext context, Material material, List<CatalogStockItemDto> stockItems,
|
ApplicationDbContext context, Material material, List<CatalogStockItemDto> stockItems,
|
||||||
Dictionary<string, int> supplierMap, ImportResultDto result)
|
ImportResultDto result)
|
||||||
{
|
{
|
||||||
var existingStockItems = await context.StockItems
|
var existingStockItems = await context.StockItems
|
||||||
.Include(s => s.SupplierOfferings)
|
|
||||||
.Where(s => s.MaterialId == material.Id)
|
.Where(s => s.MaterialId == material.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -413,20 +347,17 @@ public class CatalogService
|
|||||||
var existing = existingStockItems.FirstOrDefault(
|
var existing = existingStockItems.FirstOrDefault(
|
||||||
s => s.LengthInches == dto.LengthInches);
|
s => s.LengthInches == dto.LengthInches);
|
||||||
|
|
||||||
StockItem stockItem;
|
|
||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
existing.Name = dto.Name ?? existing.Name;
|
existing.Name = dto.Name ?? existing.Name;
|
||||||
existing.Notes = dto.Notes ?? existing.Notes;
|
existing.Notes = dto.Notes ?? existing.Notes;
|
||||||
existing.IsActive = true;
|
existing.IsActive = true;
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
stockItem = existing;
|
|
||||||
result.StockItemsUpdated++;
|
result.StockItemsUpdated++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
stockItem = new StockItem
|
var stockItem = new StockItem
|
||||||
{
|
{
|
||||||
MaterialId = material.Id,
|
MaterialId = material.Id,
|
||||||
LengthInches = dto.LengthInches,
|
LengthInches = dto.LengthInches,
|
||||||
@@ -440,56 +371,6 @@ public class CatalogService
|
|||||||
existingStockItems.Add(stockItem);
|
existingStockItems.Add(stockItem);
|
||||||
result.StockItemsCreated++;
|
result.StockItemsCreated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var offeringDto in dto.SupplierOfferings)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!supplierMap.TryGetValue(offeringDto.SupplierName, out var supplierId))
|
|
||||||
{
|
|
||||||
result.Warnings.Add(
|
|
||||||
$"Offering for stock '{material.DisplayName} @ {dto.LengthInches}\"': " +
|
|
||||||
$"Unknown supplier '{offeringDto.SupplierName}', skipped");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingOffering = stockItem.SupplierOfferings.FirstOrDefault(
|
|
||||||
o => o.SupplierId == supplierId);
|
|
||||||
|
|
||||||
if (existingOffering != null)
|
|
||||||
{
|
|
||||||
existingOffering.PartNumber = offeringDto.PartNumber ?? existingOffering.PartNumber;
|
|
||||||
existingOffering.SupplierDescription = offeringDto.SupplierDescription ?? existingOffering.SupplierDescription;
|
|
||||||
existingOffering.Price = offeringDto.Price ?? existingOffering.Price;
|
|
||||||
existingOffering.Notes = offeringDto.Notes ?? existingOffering.Notes;
|
|
||||||
existingOffering.IsActive = true;
|
|
||||||
result.OfferingsUpdated++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var offering = new SupplierOffering
|
|
||||||
{
|
|
||||||
StockItemId = stockItem.Id,
|
|
||||||
SupplierId = supplierId,
|
|
||||||
PartNumber = offeringDto.PartNumber,
|
|
||||||
SupplierDescription = offeringDto.SupplierDescription,
|
|
||||||
Price = offeringDto.Price,
|
|
||||||
Notes = offeringDto.Notes
|
|
||||||
};
|
|
||||||
context.SupplierOfferings.Add(offering);
|
|
||||||
stockItem.SupplierOfferings.Add(offering);
|
|
||||||
result.OfferingsCreated++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Errors.Add(
|
|
||||||
$"Offering for '{material.DisplayName} @ {dto.LengthInches}\"' " +
|
|
||||||
$"from '{offeringDto.SupplierName}': {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -497,24 +378,18 @@ public class CatalogService
|
|||||||
$"Stock item '{material.DisplayName} @ {dto.LengthInches}\"': {ex.Message}");
|
$"Stock item '{material.DisplayName} @ {dto.LengthInches}\"': {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<CatalogStockItemDto> MapStockItems(Material m, List<Supplier> suppliers)
|
private static List<CatalogStockItemDto> MapStockItems(Material m)
|
||||||
{
|
{
|
||||||
return m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
|
return m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
|
||||||
{
|
{
|
||||||
LengthInches = s.LengthInches,
|
LengthInches = s.LengthInches,
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
QuantityOnHand = s.QuantityOnHand,
|
||||||
Notes = s.Notes,
|
Notes = s.Notes
|
||||||
SupplierOfferings = s.SupplierOfferings.Select(o => new CatalogSupplierOfferingDto
|
|
||||||
{
|
|
||||||
SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown",
|
|
||||||
PartNumber = o.PartNumber,
|
|
||||||
SupplierDescription = o.SupplierDescription,
|
|
||||||
Price = o.Price,
|
|
||||||
Notes = o.Notes
|
|
||||||
}).ToList()
|
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
using CutList.Web.Data;
|
|
||||||
using CutList.Web.Data.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CutList.Web.Services;
|
|
||||||
|
|
||||||
public class PurchaseItemService
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ApplicationDbContext> _factory;
|
|
||||||
|
|
||||||
public PurchaseItemService(IDbContextFactory<ApplicationDbContext> factory)
|
|
||||||
{
|
|
||||||
_factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<PurchaseItem>> GetAllAsync(PurchaseItemStatus? status = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var query = context.PurchaseItems
|
|
||||||
.Include(p => p.StockItem)
|
|
||||||
.ThenInclude(s => s.Material)
|
|
||||||
.Include(p => p.Supplier)
|
|
||||||
.Include(p => p.Job)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (status.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(p => p.Status == status.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(p => p.Status)
|
|
||||||
.ThenByDescending(p => p.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PurchaseItem?> GetByIdAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
return await context.PurchaseItems
|
|
||||||
.Include(p => p.StockItem)
|
|
||||||
.ThenInclude(s => s.Material)
|
|
||||||
.Include(p => p.Supplier)
|
|
||||||
.Include(p => p.Job)
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PurchaseItem> CreateAsync(PurchaseItem item)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
item.CreatedAt = DateTime.UtcNow;
|
|
||||||
context.PurchaseItems.Add(item);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateBulkAsync(List<PurchaseItem> items)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
item.CreatedAt = now;
|
|
||||||
}
|
|
||||||
context.PurchaseItems.AddRange(items);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(PurchaseItem item)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
item.UpdatedAt = DateTime.UtcNow;
|
|
||||||
context.PurchaseItems.Update(item);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateStatusAsync(int id, PurchaseItemStatus status)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var item = await context.PurchaseItems.FindAsync(id);
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
item.Status = status;
|
|
||||||
item.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateSupplierAsync(int id, int? supplierId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var item = await context.PurchaseItems.FindAsync(id);
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
item.SupplierId = supplierId;
|
|
||||||
item.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var item = await context.PurchaseItems.FindAsync(id);
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
context.PurchaseItems.Remove(item);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,8 +57,6 @@ public class StockItemService
|
|||||||
|
|
||||||
return await context.StockItems
|
return await context.StockItems
|
||||||
.Include(s => s.Material)
|
.Include(s => s.Material)
|
||||||
.Include(s => s.SupplierOfferings)
|
|
||||||
.ThenInclude(o => o.Supplier)
|
|
||||||
.FirstOrDefaultAsync(s => s.Id == id);
|
.FirstOrDefaultAsync(s => s.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +110,7 @@ public class StockItemService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stock transaction methods
|
// Stock transaction methods
|
||||||
public async Task<StockTransaction> AddStockAsync(int stockItemId, int quantity, int? supplierId = null, decimal? unitPrice = null, string? notes = null)
|
public async Task<StockTransaction> AddStockAsync(int stockItemId, int quantity, string? notes = null)
|
||||||
{
|
{
|
||||||
await using var context = _factory.CreateDbContext();
|
await using var context = _factory.CreateDbContext();
|
||||||
|
|
||||||
@@ -124,8 +122,6 @@ public class StockItemService
|
|||||||
StockItemId = stockItemId,
|
StockItemId = stockItemId,
|
||||||
Quantity = quantity,
|
Quantity = quantity,
|
||||||
Type = StockTransactionType.Received,
|
Type = StockTransactionType.Received,
|
||||||
SupplierId = supplierId,
|
|
||||||
UnitPrice = unitPrice,
|
|
||||||
Notes = notes,
|
Notes = notes,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
@@ -139,34 +135,6 @@ public class StockItemService
|
|||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<decimal?> GetAverageCostAsync(int stockItemId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
var transactions = await context.StockTransactions
|
|
||||||
.Where(t => t.StockItemId == stockItemId && t.Type == StockTransactionType.Received && t.UnitPrice.HasValue)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (transactions.Count == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var totalCost = transactions.Sum(t => t.Quantity * t.UnitPrice!.Value);
|
|
||||||
var totalQty = transactions.Sum(t => t.Quantity);
|
|
||||||
|
|
||||||
return totalQty > 0 ? totalCost / totalQty : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<decimal?> GetLastPurchasePriceAsync(int stockItemId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
|
|
||||||
return await context.StockTransactions
|
|
||||||
.Where(t => t.StockItemId == stockItemId && t.Type == StockTransactionType.Received && t.UnitPrice.HasValue)
|
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
|
||||||
.Select(t => t.UnitPrice)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StockTransaction> UseStockAsync(int stockItemId, int quantity, int? jobId = null, string? notes = null)
|
public async Task<StockTransaction> UseStockAsync(int stockItemId, int quantity, int? jobId = null, string? notes = null)
|
||||||
{
|
{
|
||||||
await using var context = _factory.CreateDbContext();
|
await using var context = _factory.CreateDbContext();
|
||||||
@@ -251,7 +219,6 @@ public class StockItemService
|
|||||||
|
|
||||||
var query = context.StockTransactions
|
var query = context.StockTransactions
|
||||||
.Include(t => t.Job)
|
.Include(t => t.Job)
|
||||||
.Include(t => t.Supplier)
|
|
||||||
.Where(t => t.StockItemId == stockItemId)
|
.Where(t => t.StockItemId == stockItemId)
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
using CutList.Web.Data;
|
|
||||||
using CutList.Web.Data.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CutList.Web.Services;
|
|
||||||
|
|
||||||
public class SupplierService
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ApplicationDbContext> _factory;
|
|
||||||
|
|
||||||
public SupplierService(IDbContextFactory<ApplicationDbContext> factory)
|
|
||||||
{
|
|
||||||
_factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Supplier>> GetAllAsync(bool includeInactive = false)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
var query = context.Suppliers.AsQueryable();
|
|
||||||
if (!includeInactive)
|
|
||||||
{
|
|
||||||
query = query.Where(s => s.IsActive);
|
|
||||||
}
|
|
||||||
return await query.OrderBy(s => s.Name).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Supplier?> GetByIdAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
return await context.Suppliers
|
|
||||||
.Include(s => s.Offerings)
|
|
||||||
.ThenInclude(o => o.StockItem)
|
|
||||||
.ThenInclude(si => si.Material)
|
|
||||||
.FirstOrDefaultAsync(s => s.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Supplier> CreateAsync(Supplier supplier)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
supplier.CreatedAt = DateTime.UtcNow;
|
|
||||||
context.Suppliers.Add(supplier);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return supplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(Supplier supplier)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
context.Suppliers.Update(supplier);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
var supplier = await context.Suppliers.FindAsync(id);
|
|
||||||
if (supplier != null)
|
|
||||||
{
|
|
||||||
supplier.IsActive = false;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offering management
|
|
||||||
public async Task<List<SupplierOffering>> GetOfferingsForSupplierAsync(int supplierId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
return await context.SupplierOfferings
|
|
||||||
.Include(o => o.StockItem)
|
|
||||||
.ThenInclude(si => si.Material)
|
|
||||||
.Where(o => o.SupplierId == supplierId && o.IsActive)
|
|
||||||
.OrderBy(o => o.StockItem.Material.Shape)
|
|
||||||
.ThenBy(o => o.StockItem.Material.Size)
|
|
||||||
.ThenBy(o => o.StockItem.LengthInches)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<SupplierOffering>> GetOfferingsForStockItemAsync(int stockItemId)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
return await context.SupplierOfferings
|
|
||||||
.Include(o => o.Supplier)
|
|
||||||
.Where(o => o.StockItemId == stockItemId && o.IsActive && o.Supplier.IsActive)
|
|
||||||
.OrderBy(o => o.Supplier.Name)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SupplierOffering?> GetOfferingByIdAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
return await context.SupplierOfferings
|
|
||||||
.Include(o => o.StockItem)
|
|
||||||
.ThenInclude(si => si.Material)
|
|
||||||
.Include(o => o.Supplier)
|
|
||||||
.FirstOrDefaultAsync(o => o.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SupplierOffering> AddOfferingAsync(SupplierOffering offering)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
context.SupplierOfferings.Add(offering);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return offering;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateOfferingAsync(SupplierOffering offering)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
context.SupplierOfferings.Update(offering);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteOfferingAsync(int id)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
var offering = await context.SupplierOfferings.FindAsync(id);
|
|
||||||
if (offering != null)
|
|
||||||
{
|
|
||||||
offering.IsActive = false;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> OfferingExistsAsync(int supplierId, int stockItemId, int? excludeId = null)
|
|
||||||
{
|
|
||||||
await using var context = _factory.CreateDbContext();
|
|
||||||
var query = context.SupplierOfferings.Where(o =>
|
|
||||||
o.SupplierId == supplierId &&
|
|
||||||
o.StockItemId == stockItemId &&
|
|
||||||
o.IsActive);
|
|
||||||
|
|
||||||
if (excludeId.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(o => o.Id != excludeId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.AnyAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
# Remove vendor/supplier data from CutList
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
CutList's stated purpose is a 1D bin-packing optimizer: given a parts list and available stock, compute an efficient cutting plan. Supplier/vendor management (contacts, part numbers, pricing, purchase orders) was built alongside it but never touched by the packing engine itself (`AdvancedFitEngine`, `MultiBinEngine`, `CutListPackingService` never reference `Supplier`). It only entered through inventory-management pages.
|
||||||
|
|
||||||
|
This surfaced concretely in the Alro Steel catalog scraper: every scraped stock item was wrapped in a `supplierOfferings` array with empty `partNumber`/`supplierDescription` placeholders (Alro's SmartGrid never exposes part numbers), making it needlessly verbose to add or change a stock length. Investigating further surfaced the actual reported bug — a user (Carrol) couldn't figure out how to change an existing stock item's length, because `Stock/Edit.razor` hard-locks the Length field to `readonly` outside of creation.
|
||||||
|
|
||||||
|
Decision: fix the length-editing bug, and separately, pull vendor/purchasing concepts out of CutList entirely. Procurement (who to buy from, at what price, PO status) is a different domain than nesting optimization. If a "what should we order" feature is needed later, it can live in a layer above CutList that queries the existing Materials/StockItems REST API (`MaterialsController`, `StockItemsController`) to compare `JobStock` requirements against `StockItem.QuantityOnHand` — nothing in this change forecloses that.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Kept in CutList (nesting/inventory-relevant)
|
||||||
|
|
||||||
|
- **`StockItem.QuantityOnHand`** — becomes directly editable via a stripped-down `StockTransaction` log:
|
||||||
|
- Keeps: `Quantity`, `Type` (`Received`/`Used`/`Adjustment`/`Scrapped`; `Returned` is dead code today — never constructed anywhere, only referenced in a UI badge-color switch — and stays dead, out of scope to touch), `JobId`, `Notes`, `CreatedAt`.
|
||||||
|
- Drops: `SupplierId`, `UnitPrice`, and the `Supplier` navigation.
|
||||||
|
- `StockItemService` keeps `AddStockAsync/UseStockAsync/AdjustStockAsync/ScrapStockAsync/GetTransactionHistoryAsync/RecalculateQuantityAsync`, with `supplierId`/`unitPrice` parameters removed from `AddStockAsync`.
|
||||||
|
- Drops `GetAverageCostAsync`/`GetLastPurchasePriceAsync` and `StockPricingDto` entirely — there's no non-vendor version of "cost."
|
||||||
|
- **`Job.LockedAt`/`IsLocked`** — kept. Becomes a direct manual Lock/Unlock action instead of a side effect of "Add to Order List." Still production-relevant: don't let someone edit a parts list after material's been committed/cut against it.
|
||||||
|
- **Bug fix**: `Stock/Edit.razor`'s Length field becomes editable in both create and edit modes. The backend already supports this — `StockItemService.ExistsAsync` already excludes the current row's ID for the edit case in its `(MaterialId, LengthInches)` uniqueness check — so this is a front-end-only fix.
|
||||||
|
|
||||||
|
### Removed entirely (procurement layer, not CutList's job)
|
||||||
|
|
||||||
|
- **Entities**: `Supplier`, `SupplierOffering`, `PurchaseItem`.
|
||||||
|
- **Services/Controllers/DTOs**: `SupplierService`, `PurchaseItemService`, `SuppliersController`, `SupplierDtos.cs`. `StockItemsController` drops its `/offerings` and `/pricing` endpoints.
|
||||||
|
- **Pages**: `Suppliers/Index.razor`, `Suppliers/Edit.razor`, `Orders/Index.razor`, `Orders/Add.razor`. `NavMenu.razor` loses the Suppliers and Orders links. `Home.razor` loses the Suppliers dashboard card and supplier/order mentions in the getting-started copy.
|
||||||
|
- **`Jobs/Edit.razor`**: "Add to Order List" button (which created `PurchaseItem`s and locked the job) is replaced with a plain "Lock Job" button that calls `JobService.LockAsync` directly. Drops the `PurchaseItemService` dependency and bulk-create logic.
|
||||||
|
- **Catalog import/export**: `CatalogDtos.cs` drops `CatalogSupplierDto`, `CatalogSupplierOfferingDto`, `CatalogData.Suppliers`, and `CatalogStockItemDto.SupplierOfferings`. `CatalogService` drops `ImportSuppliersAsync` and all offering-import logic. Stock items in the catalog format become `{ lengthInches, quantityOnHand }`.
|
||||||
|
- **Seed data**: `alro-catalog.json` already regenerated without vendor wrappers. `oneals-catalog.json` gets the same treatment via a one-off transform script (not checked in as a permanent tool) — it currently has *real* O'Neal part numbers/prices, which will be discarded along with the empty Alro placeholders, for consistency.
|
||||||
|
- **`CutList.Mcp`**: removes `list_suppliers`, `add_supplier`, `list_supplier_offerings`, `add_supplier_offering`, `add_stock_with_offering` (tools, DTOs, and the corresponding `ApiClient` methods). Adds a plain `add_stock` tool: find-or-create material → find-or-create stock item → set quantity — same convenience flow as `add_stock_with_offering` minus the offering step. Republished to `~/.claude/mcp/CutList.Mcp/` per the standard MCP publishing workflow.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
One new EF Core migration:
|
||||||
|
- Drops `Suppliers`, `SupplierOfferings`, `PurchaseItems` tables.
|
||||||
|
- Drops `SupplierId`/`UnitPrice` columns and the associated FK from `StockTransactions`.
|
||||||
|
|
||||||
|
This is destructive to whatever currently lives in those tables (e.g., any real Supplier/Offering/PurchaseItem rows in the dev DB). Applied immediately after creation per the usual EF workflow, no separate confirmation gate beyond this design doc.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Building the "layer above" that would query stock levels for purchasing decisions — not needed now, and the existing Materials/StockItems REST API already exposes what such a layer would need later.
|
||||||
|
- Any change to the packing algorithm itself (`CutList.Core`) — untouched by this work, confirming it never depended on vendor data in the first place.
|
||||||
|
- Removing the dead `StockTransactionType.Returned` enum value — unused, harmless, unrelated to vendor data.
|
||||||
Reference in New Issue
Block a user