Compare commits
35 Commits
21d50e7c20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d3c92226c | |||
| c31769a746 | |||
| f04bf02c42 | |||
| dac2833dd1 | |||
| a226a1f652 | |||
| 5000021193 | |||
| 02e936febb | |||
| e13f876da6 | |||
| 1f3eb67eb7 | |||
| 2fdf006a8e | |||
| eee38a8473 | |||
| 59f86c8e79 | |||
| 891b214b29 | |||
| c5f366a3ef | |||
| 8926d44969 | |||
| c23c92e852 | |||
| 2586f99c63 | |||
| 5f4e36c688 | |||
| ed705625e9 | |||
| 1ccdeb6817 | |||
| 69b282aaf3 | |||
| f932e8ba13 | |||
| 141176cc5d | |||
| 3fd354aff0 | |||
| b603a4b3e7 | |||
| 5468b2748d | |||
| 8ed10939d4 | |||
| 2a94ad63cb | |||
| b0a9d7fdcc | |||
| f20770d03e | |||
| 4aec4c2275 | |||
| 261f64a895 | |||
| 9b757acac3 | |||
| 177affabf0 | |||
| 17f16901ef |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.GC.Server": true,
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
246
CLAUDE.md
246
CLAUDE.md
@@ -1,13 +1,22 @@
|
||||
# CLAUDE.md
|
||||
|
||||
> **IMPORTANT**: Always keep this document updated when functionality changes, entities are added/modified, new pages or services are created, or architectural patterns evolve.
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
CutList is a Windows Forms 1D bin packing optimization application that helps users optimize material cutting. It calculates efficient bin packing solutions to minimize waste when cutting stock materials into required parts.
|
||||
CutList is a 1D bin packing optimization application that helps users optimize material cutting. It calculates efficient bin packing solutions to minimize waste when cutting stock materials into required parts.
|
||||
|
||||
**Framework**: .NET 8.0 (Windows Forms)
|
||||
**Key Dependencies**: Math-Expression-Evaluator (input parsing), Newtonsoft.Json (serialization)
|
||||
The solution contains three projects:
|
||||
|
||||
| Project | Framework | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| **CutList** | .NET 8.0 Windows Forms | Original desktop UI (MVP pattern) |
|
||||
| **CutList.Core** | .NET 8.0 Class Library | Domain models and packing algorithms (platform-agnostic) |
|
||||
| **CutList.Web** | .NET 8.0 Blazor Server | Web-based UI with EF Core + SQL Server |
|
||||
|
||||
**Key Dependencies**: Math-Expression-Evaluator (input parsing), Newtonsoft.Json (serialization), Entity Framework Core (data access), Bootstrap 5 + Bootstrap Icons (UI)
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -15,12 +24,18 @@ CutList is a Windows Forms 1D bin packing optimization application that helps us
|
||||
# Build entire solution
|
||||
dotnet build CutList.sln
|
||||
|
||||
# Build specific project
|
||||
# Build specific projects
|
||||
dotnet build CutList/CutList.csproj
|
||||
dotnet build CutList.Core/CutList.Core.csproj
|
||||
dotnet build CutList.Web/CutList.Web.csproj
|
||||
|
||||
# Run the application
|
||||
dotnet run --project CutList/CutList.csproj
|
||||
# Run applications
|
||||
dotnet run --project CutList/CutList.csproj # WinForms
|
||||
dotnet run --project CutList.Web/CutList.Web.csproj # Blazor
|
||||
|
||||
# EF Core migrations (always apply immediately after creating)
|
||||
dotnet ef migrations add <Name> --project CutList.Web
|
||||
dotnet ef database update --project CutList.Web
|
||||
|
||||
# Clean build
|
||||
dotnet clean CutList.sln
|
||||
@@ -28,69 +43,182 @@ dotnet clean CutList.sln
|
||||
|
||||
## Architecture
|
||||
|
||||
### Project Structure
|
||||
### CutList.Core — Domain & Algorithms
|
||||
|
||||
- **CutList/** - Main Windows Forms application (UI layer)
|
||||
- **CutList.Core/** - Core library with domain models and packing algorithms (shareable, platform-agnostic)
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**MVP (Model-View-Presenter)**
|
||||
- `MainForm` implements `IMainView` (pure UI, no business logic)
|
||||
- `MainFormPresenter` orchestrates all business logic
|
||||
- `Document` contains application state
|
||||
|
||||
**Result Pattern**
|
||||
- `Result<T>` in Common/Result.cs for standardized error handling
|
||||
- Use `Result<T>.Success(value)` and `Result<T>.Failure(error)` instead of exceptions for expected errors
|
||||
|
||||
**Factory Pattern**
|
||||
- `IEngineFactory` creates packing engines
|
||||
- Allows swapping algorithm implementations
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. User enters parts/bins in MainForm
|
||||
2. MainFormPresenter calls CutListService.Pack()
|
||||
3. CutListService converts input models to core models (MultiBin, BinItem)
|
||||
4. MultiBinEngine coordinates packing across bin types
|
||||
5. AdvancedFitEngine performs 1D bin packing (first-fit decreasing with optimization)
|
||||
6. Results displayed in ResultsForm
|
||||
|
||||
### Key Services
|
||||
|
||||
- **CutListService**: Bridges UI models to core packing algorithms
|
||||
- **DocumentService**: JSON file persistence
|
||||
|
||||
### Domain Models (CutList.Core)
|
||||
|
||||
- **BinItem**: Item to be packed (with label, length)
|
||||
**Key Domain Models**:
|
||||
- **BinItem**: Item to be packed (label, length)
|
||||
- **MultiBin**: Stock bin type with length, quantity (-1 = unlimited), priority
|
||||
- **Bin**: Packed bin containing items
|
||||
- **Bin**: Packed bin containing items, tracks remaining length
|
||||
- **Tool**: Cutting tool with kerf/blade width
|
||||
|
||||
### Unit Handling
|
||||
**Packing Engine**:
|
||||
- `MultiBinEngine` coordinates packing across bin types
|
||||
- `AdvancedFitEngine` performs 1D bin packing (first-fit decreasing with optimization)
|
||||
- `PackingStrategy.AdvancedFit` is the standard strategy
|
||||
|
||||
- **ArchUnits**: Converts feet/inches/fractions to decimals (accepts "12'", "6\"", "12 1/2\"", etc.)
|
||||
- **FormatHelper**: Converts decimals to mixed fractions for display
|
||||
**Unit Handling**:
|
||||
- `ArchUnits` — Converts feet/inches/fractions to decimal inches (accepts "12'", "6\"", "12 1/2\"", etc.)
|
||||
- `FormatHelper` — Converts decimals to mixed fractions for display
|
||||
- Internal calculations use inches; format on display
|
||||
|
||||
## Important Conventions
|
||||
**Patterns**:
|
||||
- `Result<T>` for standardized error handling (Success/Failure instead of exceptions)
|
||||
- `IEngineFactory` for swappable algorithm implementations
|
||||
- Lower priority number = used first in bin selection
|
||||
|
||||
- **Nullable reference types enabled** - handle nulls explicitly
|
||||
- **Collections are encapsulated** - use AsReadOnly(), access via Add* methods
|
||||
- **Validation in domain models** - constructors and properties validate inputs
|
||||
- **Parameterless constructors** on Tool/MultiBin are for JSON serialization only
|
||||
- **Spacing property** on engines handles blade/kerf width
|
||||
- **Priority system**: Lower priority bins are used first
|
||||
### CutList (WinForms) — Desktop UI
|
||||
|
||||
- MVP pattern: `MainForm` implements `IMainView`, `MainFormPresenter` orchestrates logic
|
||||
- `Document` holds application state
|
||||
- `CutListService` bridges UI models to core packing algorithms
|
||||
- `DocumentService` handles JSON file persistence
|
||||
|
||||
### CutList.Web (Blazor Server) — Web UI
|
||||
|
||||
**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
|
||||
|
||||
## CutList.Web Entities
|
||||
|
||||
### Material
|
||||
- `Shape` (MaterialShape enum): Round Bar, Round Tube, Flat Bar, Square Bar, Square Tube, Rectangular Tube, Angle, Channel, I-Beam, Pipe
|
||||
- `Type` (MaterialType enum): Steel, Aluminum, Stainless, Brass, Copper
|
||||
- `Grade` (string?), `Size` (string), `Description` (string?), `IsActive` (bool), `SortOrder` (int)
|
||||
- **DisplayName**: "{Shape} - {Size}"
|
||||
- **Relationships**: `Dimensions` (1:1 MaterialDimensions), `StockItems` (1:many), `JobParts` (1:many)
|
||||
|
||||
### MaterialDimensions (TPC Inheritance)
|
||||
Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its own standalone table (`DimAngle`, `DimChannel`, `DimFlatBar`, `DimIBeam`, `DimPipe`, `DimRectangularTube`, `DimRoundBar`, `DimRoundTube`, `DimSquareBar`, `DimSquareTube`) with no base table. Each table has its own `Id` (shared sequence) and `MaterialId` FK. Each generates its own `SizeString` and `SortOrder`.
|
||||
|
||||
### StockItem
|
||||
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`
|
||||
- **Unique constraint**: (MaterialId, LengthInches)
|
||||
- **Relationships**: `Material`, `SupplierOfferings` (1:many), `Transactions` (1:many StockTransaction)
|
||||
|
||||
### StockTransaction
|
||||
- `StockItemId`, `Quantity` (signed delta), `Type` (Received/Used/Adjustment/Scrapped/Returned)
|
||||
- Optional: `JobId`, `SupplierId`, `UnitPrice`
|
||||
|
||||
### 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
|
||||
- `Name`, `KerfInches` (decimal), `IsDefault` (bool), `IsActive`
|
||||
- **Seeded**: Bandsaw (0.0625"), Chop Saw (0.125"), Cold Cut Saw (0.0625"), Hacksaw (0.0625")
|
||||
|
||||
### Job
|
||||
- `JobNumber` (auto-generated "JOB-#####", unique), `Name`, `Customer`, `CuttingToolId`, `Notes`
|
||||
- `LockedAt` (DateTime?) — set when materials ordered; `IsLocked` computed property
|
||||
- `OptimizationResultJson` (string?, nvarchar(max)) — serialized optimization results
|
||||
- `OptimizedAt` (DateTime?) — when optimization was last run
|
||||
- **Relationships**: `Parts` (1:many JobPart), `Stock` (1:many JobStock), `CuttingTool`
|
||||
|
||||
### JobPart
|
||||
- `JobId`, `MaterialId`, `Name`, `LengthInches` (decimal), `Quantity` (int), `SortOrder`
|
||||
|
||||
### JobStock
|
||||
- `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
|
||||
|
||||
### MaterialService
|
||||
- CRUD with soft delete, dimension management (`CreateWithDimensionsAsync`, `UpdateWithDimensionsAsync`)
|
||||
- Search by dimension (e.g., `SearchRoundBarByDiameterAsync`, `SearchAngleByLegAsync`) with tolerance
|
||||
- `CreateDimensionsForShape(shape)` factory method
|
||||
|
||||
### StockItemService
|
||||
- CRUD with soft delete
|
||||
- Stock transactions: `AddStockAsync`, `UseStockAsync`, `AdjustStockAsync`, `ScrapStockAsync`
|
||||
- `GetTransactionHistoryAsync`, `RecalculateQuantityAsync`
|
||||
- Pricing: `GetAverageCostAsync`, `GetLastPurchasePriceAsync`
|
||||
|
||||
### SupplierService
|
||||
- CRUD for suppliers and offerings
|
||||
- `GetOfferingsForStockItemAsync` — all supplier options for a stock item
|
||||
|
||||
### JobService
|
||||
- Job CRUD: `CreateAsync` (auto-generates JobNumber), `DuplicateAsync` (deep copy), `QuickCreateAsync`
|
||||
- Lock/Unlock: `LockAsync(id)`, `UnlockAsync(id)` — controls job editability after ordering
|
||||
- Parts: `AddPartAsync`, `UpdatePartAsync`, `DeletePartAsync` (all update job timestamp + clear optimization results)
|
||||
- Stock: `AddStockAsync`, `UpdateStockAsync`, `DeleteStockAsync` (all clear optimization results)
|
||||
- Optimization: `SaveOptimizationResultAsync`, `ClearOptimizationResultAsync`
|
||||
- Cutting tools: full CRUD with single-default enforcement
|
||||
|
||||
### CutListPackingService
|
||||
- `PackAsync(parts, kerfInches, jobStock?)` — runs optimization per material group
|
||||
- Separates results into `InStockBins` (from inventory) and `ToBePurchasedBins`
|
||||
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
||||
- `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
|
||||
- `FormatLength(inches)`, `GroupItems(items)` for print report formatting
|
||||
|
||||
## CutList.Web Pages
|
||||
|
||||
| Route | Page | Purpose |
|
||||
|-------|------|---------|
|
||||
| `/` | Home | Welcome page with feature cards and workflow guide |
|
||||
| `/jobs` | Jobs/Index | Job list with pagination, lock icons, Quick Create, Duplicate, Delete |
|
||||
| `/jobs/new` | Jobs/Edit | New job form (details only) |
|
||||
| `/jobs/{Id}` | Jobs/Edit | Tabbed editor (Details, Parts, Stock, Results); locked jobs show banner + disable editing |
|
||||
| `/materials` | Materials/Index | Material list with MaterialFilter, pagination |
|
||||
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
||||
| `/stock` | Stock/Index | Stock items with MaterialFilter, quantity badges |
|
||||
| `/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 |
|
||||
|
||||
## Shared Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `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` |
|
||||
| `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 |
|
||||
|
||||
## Key Patterns & Conventions
|
||||
|
||||
- **Nullable reference types enabled** — handle nulls explicitly
|
||||
- **Soft deletes** — Materials, Suppliers, 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
|
||||
- **Pagination** — All list pages use `Pager` with `pageSize = 25`
|
||||
- **ConfirmDialog** — All destructive actions use the shared `ConfirmDialog` component
|
||||
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
||||
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
||||
- **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
|
||||
- **Purchase flow** — Optimize job -> "Add to Order List" creates PurchaseItems + locks job -> Orders page manages status (Pending -> Ordered -> Received)
|
||||
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
||||
- **Collections** — Encapsulated in Core; use `AsReadOnly()`, access via `Add*` methods
|
||||
- **Priority system** — Lower priority bins used first in packing algorithm
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Presenters/MainFormPresenter.cs` | Main business logic orchestrator |
|
||||
| `Services/CutListService.cs` | Packing algorithm interface |
|
||||
| `CutList.Core/Nesting/AdvancedFitEngine.cs` | Core packing algorithm |
|
||||
| `CutList.Core/Nesting/MultiBinEngine.cs` | Multi-bin orchestration |
|
||||
| `CutList.Core/ArchUnits.cs` | Unit conversion |
|
||||
| `CutList.Core/FormatHelper.cs` | Output formatting |
|
||||
| `CutList.Core/Nesting/AdvancedFitEngine.cs` | Core 1D bin packing algorithm |
|
||||
| `CutList.Core/Nesting/MultiBinEngine.cs` | Multi-bin type orchestration |
|
||||
| `CutList.Core/ArchUnits.cs` | Architectural unit parsing/conversion |
|
||||
| `CutList.Core/Formatting/FormatHelper.cs` | Display formatting |
|
||||
| `CutList.Web/Data/ApplicationDbContext.cs` | EF Core context with all DbSets and configuration |
|
||||
| `CutList.Web/Services/JobService.cs` | Job orchestration (CRUD, parts, stock, tools, lock/unlock) |
|
||||
| `CutList.Web/Services/CutListPackingService.cs` | Bridges web entities to Core packing engine |
|
||||
| `CutList.Web/Components/Pages/Jobs/Edit.razor` | Job editor (tabbed: Details, Parts, Stock, Results) |
|
||||
| `CutList/Presenters/MainFormPresenter.cs` | WinForms business logic orchestrator |
|
||||
|
||||
@@ -9,9 +9,9 @@ namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Default maximum number of items before falling back to AdvancedFitEngine.
|
||||
/// Testing showed 20 items is safe (~100ms worst case), while 21+ can take seconds.
|
||||
/// Testing showed 25 items is safe (~84ms worst case), while 30+ can take seconds.
|
||||
/// </summary>
|
||||
public const int DefaultMaxItems = 20;
|
||||
public const int DefaultMaxItems = 25;
|
||||
|
||||
private readonly IEngine _fallbackEngine;
|
||||
private readonly int _maxItems;
|
||||
@@ -67,7 +67,15 @@ namespace CutList.Core.Nesting
|
||||
BinCount = 0
|
||||
};
|
||||
|
||||
Search(sortedItems, 0, currentState, bestSolution, request);
|
||||
// Precompute suffix sums of item lengths (including spacing per item)
|
||||
// for lower-bound pruning. suffixVolume[i] = total volume of items[i..n-1].
|
||||
var suffixVolume = new double[sortedItems.Count + 1];
|
||||
for (int i = sortedItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
suffixVolume[i] = suffixVolume[i + 1] + sortedItems[i].Length + request.Spacing;
|
||||
}
|
||||
|
||||
Search(sortedItems, 0, currentState, bestSolution, request, suffixVolume);
|
||||
|
||||
// Build result from best solution
|
||||
var result = new PackResult();
|
||||
@@ -101,7 +109,8 @@ namespace CutList.Core.Nesting
|
||||
int itemIndex,
|
||||
SearchState current,
|
||||
SearchState best,
|
||||
PackingRequest request)
|
||||
PackingRequest request,
|
||||
double[] suffixVolume)
|
||||
{
|
||||
// All items placed - check if this is better
|
||||
if (itemIndex >= items.Count)
|
||||
@@ -123,6 +132,18 @@ namespace CutList.Core.Nesting
|
||||
if (current.BinCount >= request.MaxBinCount)
|
||||
return;
|
||||
|
||||
// Lower-bound pruning: remaining items need at least this many additional bins
|
||||
double remainingVolume = suffixVolume[itemIndex];
|
||||
double availableInExisting = 0;
|
||||
for (int b = 0; b < current.Bins.Count; b++)
|
||||
{
|
||||
availableInExisting += request.StockLength - GetBinUsedLength(current.Bins[b], request.Spacing);
|
||||
}
|
||||
double overflow = remainingVolume - availableInExisting;
|
||||
int additionalBinsNeeded = overflow > 0 ? (int)Math.Ceiling(overflow / request.StockLength) : 0;
|
||||
if (current.BinCount + additionalBinsNeeded >= best.BinCount)
|
||||
return;
|
||||
|
||||
var item = items[itemIndex];
|
||||
|
||||
// Symmetry breaking: if this item has the same length as the previous item,
|
||||
@@ -148,7 +169,7 @@ namespace CutList.Core.Nesting
|
||||
current.Bins[i].Add(item);
|
||||
var prevBinIndex = current.LastBinIndexUsed;
|
||||
current.LastBinIndexUsed = i;
|
||||
Search(items, itemIndex + 1, current, best, request);
|
||||
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||
current.LastBinIndexUsed = prevBinIndex;
|
||||
current.Bins[i].RemoveAt(current.Bins[i].Count - 1);
|
||||
}
|
||||
@@ -162,7 +183,7 @@ namespace CutList.Core.Nesting
|
||||
current.BinCount++;
|
||||
var prevBinIndex = current.LastBinIndexUsed;
|
||||
current.LastBinIndexUsed = newBinIndex;
|
||||
Search(items, itemIndex + 1, current, best, request);
|
||||
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||
current.LastBinIndexUsed = prevBinIndex;
|
||||
current.Bins.RemoveAt(current.Bins.Count - 1);
|
||||
current.BinCount--;
|
||||
|
||||
444
CutList.Mcp/ApiClient.cs
Normal file
444
CutList.Mcp/ApiClient.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace CutList.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// Typed HTTP client for calling the CutList.Web REST API.
|
||||
/// </summary>
|
||||
public class ApiClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public ApiClient(HttpClient 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
|
||||
|
||||
public async Task<List<ApiMaterialDto>> GetMaterialsAsync(string? shape = null, bool includeInactive = false)
|
||||
{
|
||||
var url = $"api/materials?includeInactive={includeInactive}";
|
||||
if (!string.IsNullOrEmpty(shape))
|
||||
url += $"&shape={Uri.EscapeDataString(shape)}";
|
||||
return await _http.GetFromJsonAsync<List<ApiMaterialDto>>(url) ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiMaterialDto?> CreateMaterialAsync(string shape, string? size, string? description,
|
||||
string? type, string? grade, Dictionary<string, decimal>? dimensions)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
Shape = shape,
|
||||
Size = size,
|
||||
Description = description,
|
||||
Type = type,
|
||||
Grade = grade,
|
||||
Dimensions = dimensions
|
||||
};
|
||||
var response = await _http.PostAsJsonAsync("api/materials", 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<ApiMaterialDto>();
|
||||
}
|
||||
|
||||
public async Task<List<ApiMaterialDto>> SearchMaterialsAsync(string shape, decimal targetValue, decimal tolerance)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("api/materials/search", new
|
||||
{
|
||||
Shape = shape,
|
||||
TargetValue = targetValue,
|
||||
Tolerance = tolerance
|
||||
});
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<ApiMaterialDto>>() ?? [];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock Items
|
||||
|
||||
public async Task<List<ApiStockItemDto>> GetStockItemsAsync(int? materialId = null, bool includeInactive = false)
|
||||
{
|
||||
var url = $"api/stock-items?includeInactive={includeInactive}";
|
||||
if (materialId.HasValue)
|
||||
url += $"&materialId={materialId.Value}";
|
||||
return await _http.GetFromJsonAsync<List<ApiStockItemDto>>(url) ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, int quantityOnHand, string? notes)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
MaterialId = materialId,
|
||||
Length = length,
|
||||
Name = name,
|
||||
QuantityOnHand = quantityOnHand,
|
||||
Notes = notes
|
||||
};
|
||||
var response = await _http.PostAsJsonAsync("api/stock-items", 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<ApiStockItemDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Jobs
|
||||
|
||||
public async Task<List<ApiJobDto>> GetJobsAsync()
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiJobDto>>("api/jobs") ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> GetJobAsync(int id)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<ApiJobDetailDto>($"api/jobs/{id}");
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> CreateJobAsync(string? name, string? customer, int? cuttingToolId, string? notes)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("api/jobs", new { Name = name, Customer = customer, CuttingToolId = cuttingToolId, Notes = notes });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobDetailDto>();
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> UpdateJobAsync(int id, string? name, string? customer, int? cuttingToolId, string? notes)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"api/jobs/{id}", new { Name = name, Customer = customer, CuttingToolId = cuttingToolId, Notes = notes });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobDetailDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobAsync(int id)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiJobPartDto?> AddJobPartAsync(int jobId, int materialId, string name, string length, int quantity)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/parts", new { MaterialId = materialId, Name = name, Length = length, Quantity = quantity });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobPartDto>();
|
||||
}
|
||||
|
||||
public async Task<ApiJobPartDto?> UpdateJobPartAsync(int jobId, int partId, int? materialId, string? name, string? length, int? quantity)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"api/jobs/{jobId}/parts/{partId}", new { MaterialId = materialId, Name = name, Length = length, Quantity = quantity });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobPartDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobPartAsync(int jobId, int partId)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{jobId}/parts/{partId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiJobStockDto?> AddJobStockAsync(int jobId, int materialId, int? stockItemId, string length, int quantity, bool isCustomLength, int priority)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/stock", new
|
||||
{
|
||||
MaterialId = materialId,
|
||||
StockItemId = stockItemId,
|
||||
Length = length,
|
||||
Quantity = quantity,
|
||||
IsCustomLength = isCustomLength,
|
||||
Priority = priority
|
||||
});
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobStockDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobStockAsync(int jobId, int stockId)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{jobId}/stock/{stockId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiPackResponseDto?> PackJobAsync(int jobId, decimal? kerfOverride = null)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/pack", new { KerfOverride = kerfOverride });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiPackResponseDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cutting Tools
|
||||
|
||||
public async Task<List<ApiCuttingToolDto>> GetCuttingToolsAsync(bool includeInactive = false)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiCuttingToolDto>>($"api/cutting-tools?includeInactive={includeInactive}") ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiCuttingToolDto?> GetDefaultCuttingToolAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetFromJsonAsync<ApiCuttingToolDto>("api/cutting-tools/default");
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#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>
|
||||
/// Thrown when the API returns 409 Conflict (duplicate resource).
|
||||
/// </summary>
|
||||
public class ApiConflictException : Exception
|
||||
{
|
||||
public ApiConflictException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
#region API Response DTOs — Jobs & Cutting Tools
|
||||
|
||||
public class ApiJobDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
}
|
||||
|
||||
public class ApiJobDetailDto : ApiJobDto
|
||||
{
|
||||
public List<ApiJobPartDto> Parts { get; set; } = new();
|
||||
public List<ApiJobStockDto> Stock { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiJobPartDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class ApiJobStockDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int? StockItemId { get; set; }
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class ApiCuttingToolDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class ApiPackResponseDto
|
||||
{
|
||||
public List<ApiMaterialPackResultDto> Materials { get; set; } = new();
|
||||
public ApiPackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiMaterialPackResultDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public List<ApiPackedBinDto> InStockBins { get; set; } = new();
|
||||
public List<ApiPackedBinDto> ToBePurchasedBins { get; set; } = new();
|
||||
public List<ApiPackedItemDto> ItemsNotPlaced { get; set; } = new();
|
||||
public ApiMaterialPackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiPackedBinDto
|
||||
{
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public double UsedInches { get; set; }
|
||||
public string UsedFormatted { get; set; } = string.Empty;
|
||||
public double WasteInches { get; set; }
|
||||
public string WasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public List<ApiPackedItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiPackedItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ApiPackingSummaryDto
|
||||
{
|
||||
public int TotalInStockBins { get; set; }
|
||||
public int TotalToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public string TotalMaterialFormatted { get; set; } = string.Empty;
|
||||
public double TotalUsedInches { get; set; }
|
||||
public string TotalUsedFormatted { get; set; } = string.Empty;
|
||||
public double TotalWasteInches { get; set; }
|
||||
public string TotalWasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public int TotalItemsNotPlaced { get; set; }
|
||||
public List<ApiMaterialPackingSummaryDto> MaterialSummaries { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiMaterialPackingSummaryDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int InStockBins { get; set; }
|
||||
public int ToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public double TotalUsedInches { get; set; }
|
||||
public double TotalWasteInches { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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 int Id { get; set; }
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? Grade { get; set; }
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public ApiMaterialDimensionsDto? Dimensions { get; set; }
|
||||
}
|
||||
|
||||
public class ApiMaterialDimensionsDto
|
||||
{
|
||||
public string DimensionType { get; set; } = string.Empty;
|
||||
public Dictionary<string, decimal> Values { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiStockItemDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public int QuantityOnHand { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class 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
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
||||
<ProjectReference Include="..\CutList.Web\CutList.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
619
CutList.Mcp/JobTools.cs
Normal file
619
CutList.Mcp/JobTools.cs
Normal file
@@ -0,0 +1,619 @@
|
||||
using System.ComponentModel;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace CutList.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// MCP tools for job management - creating jobs, managing parts/stock, and running optimization.
|
||||
/// All calls go through the CutList.Web REST API via ApiClient.
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class JobTools
|
||||
{
|
||||
private readonly ApiClient _api;
|
||||
|
||||
public JobTools(ApiClient api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
[McpServerTool(Name = "list_jobs"), Description("Lists all jobs in the system with summary info (job number, name, customer, part/stock counts).")]
|
||||
public async Task<JobListResult> ListJobs()
|
||||
{
|
||||
var jobs = await _api.GetJobsAsync();
|
||||
|
||||
return new JobListResult
|
||||
{
|
||||
Success = true,
|
||||
Jobs = jobs.Select(j => new JobSummaryDto
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingToolName,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.PartCount,
|
||||
StockCount = j.StockCount
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "get_job"), Description("Gets full job details including all parts and stock assignments.")]
|
||||
public async Task<JobDetailResult> GetJob(
|
||||
[Description("Job ID")]
|
||||
int jobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.GetJobAsync(jobId);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "create_job"), Description("Creates a new job. Returns the created job with its auto-generated job number.")]
|
||||
public async Task<JobDetailResult> CreateJob(
|
||||
[Description("Job name/description")]
|
||||
string? name = null,
|
||||
[Description("Customer name")]
|
||||
string? customer = null,
|
||||
[Description("Cutting tool ID (use list_cutting_tools to find IDs). If not set, uses the default tool.")]
|
||||
int? cuttingToolId = null,
|
||||
[Description("Notes about the job")]
|
||||
string? notes = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.CreateJobAsync(name, customer, cuttingToolId, notes);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = "Failed to create job" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "update_job"), Description("Updates job details (name, customer, cutting tool, notes). Only provided fields are updated.")]
|
||||
public async Task<JobDetailResult> UpdateJob(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("New job name")]
|
||||
string? name = null,
|
||||
[Description("New customer name")]
|
||||
string? customer = null,
|
||||
[Description("New cutting tool ID")]
|
||||
int? cuttingToolId = null,
|
||||
[Description("New notes")]
|
||||
string? notes = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.UpdateJobAsync(jobId, name, customer, cuttingToolId, notes);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job"), Description("Deletes a job and all its parts and stock assignments.")]
|
||||
public async Task<SimpleResult> DeleteJob(
|
||||
[Description("Job ID")]
|
||||
int jobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobAsync(jobId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parts
|
||||
|
||||
[McpServerTool(Name = "add_job_part"), Description("Adds a single part to a job.")]
|
||||
public async Task<JobPartResult> AddJobPart(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Material ID (use list_materials to find IDs)")]
|
||||
int materialId,
|
||||
[Description("Part name/label (e.g., 'Top Rail', 'Picket')")]
|
||||
string name,
|
||||
[Description("Part length (e.g., '36\"', '4\\' 6\"', '54.5')")]
|
||||
string length,
|
||||
[Description("Quantity needed (default 1)")]
|
||||
int quantity = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var part = await _api.AddJobPartAsync(jobId, materialId, name, length, quantity);
|
||||
if (part == null)
|
||||
return new JobPartResult { Success = false, Error = "Failed to add part" };
|
||||
|
||||
return new JobPartResult
|
||||
{
|
||||
Success = true,
|
||||
Part = MapPart(part)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobPartResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "add_job_parts"), Description("Batch adds multiple parts to a job. Ideal for entering a full BOM (bill of materials). Returns the complete job state after all parts are added.")]
|
||||
public async Task<JobDetailResult> AddJobParts(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Array of parts to add. Each needs: materialId (int), name (string), length (string like \"36\\\"\" or \"4' 6\\\"\"), quantity (int)")]
|
||||
PartEntry[] parts)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
int added = 0;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.AddJobPartAsync(jobId, part.MaterialId, part.Name, part.Length, part.Quantity);
|
||||
added++;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
errors.Add($"Failed to add '{part.Name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the full job state
|
||||
try
|
||||
{
|
||||
var job = await _api.GetJobAsync(jobId);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found after adding parts" };
|
||||
|
||||
var result = new JobDetailResult
|
||||
{
|
||||
Success = errors.Count == 0,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
|
||||
if (errors.Count > 0)
|
||||
result.Error = $"Added {added}/{parts.Length} parts. Errors: {string.Join("; ", errors)}";
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job_part"), Description("Removes a part from a job.")]
|
||||
public async Task<SimpleResult> DeleteJobPart(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Part ID (from get_job results)")]
|
||||
int partId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobPartAsync(jobId, partId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock
|
||||
|
||||
[McpServerTool(Name = "add_job_stock"), Description("Adds a stock material assignment to a job. Stock defines what material lengths are available for cutting.")]
|
||||
public async Task<JobStockResult> AddJobStock(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Material ID (must match the material used by parts)")]
|
||||
int materialId,
|
||||
[Description("Stock length (e.g., '20'', '240', '20 ft')")]
|
||||
string length,
|
||||
[Description("Quantity available (-1 for unlimited, default -1)")]
|
||||
int quantity = -1,
|
||||
[Description("Stock item ID from inventory (optional - links to tracked inventory)")]
|
||||
int? stockItemId = null,
|
||||
[Description("True if this is a custom length not from inventory (default false)")]
|
||||
bool isCustomLength = false,
|
||||
[Description("Priority - lower number = used first (default 10)")]
|
||||
int priority = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stock = await _api.AddJobStockAsync(jobId, materialId, stockItemId, length, quantity, isCustomLength, priority);
|
||||
if (stock == null)
|
||||
return new JobStockResult { Success = false, Error = "Failed to add stock" };
|
||||
|
||||
return new JobStockResult
|
||||
{
|
||||
Success = true,
|
||||
Stock = MapStock(stock)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobStockResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job_stock"), Description("Removes a stock assignment from a job.")]
|
||||
public async Task<SimpleResult> DeleteJobStock(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Stock ID (from get_job results)")]
|
||||
int stockId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobStockAsync(jobId, stockId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimization
|
||||
|
||||
[McpServerTool(Name = "optimize_job"), Description("Runs bin packing optimization on a job. The job must have parts defined. If stock is defined, it will be used; otherwise the optimizer uses available inventory. Returns optimized cut layouts per material with efficiency stats.")]
|
||||
public async Task<OptimizeJobResult> OptimizeJob(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Optional kerf override in inches (e.g., 0.125). If not set, uses the job's cutting tool kerf.")]
|
||||
double? kerfOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _api.PackJobAsync(jobId, kerfOverride.HasValue ? (decimal)kerfOverride.Value : null);
|
||||
if (result == null)
|
||||
return new OptimizeJobResult { Success = false, Error = "Optimization returned no results" };
|
||||
|
||||
return new OptimizeJobResult
|
||||
{
|
||||
Success = true,
|
||||
Materials = result.Materials.Select(m => new OptMaterialResultDto
|
||||
{
|
||||
MaterialId = m.MaterialId,
|
||||
MaterialName = m.MaterialName,
|
||||
InStockBins = m.InStockBins.Select(MapBin).ToList(),
|
||||
ToBePurchasedBins = m.ToBePurchasedBins.Select(MapBin).ToList(),
|
||||
ItemsNotPlaced = m.ItemsNotPlaced.Select(i => new OptItemDto { Name = i.Name, LengthInches = i.LengthInches, LengthFormatted = i.LengthFormatted }).ToList(),
|
||||
Summary = MapMaterialSummary(m.Summary)
|
||||
}).ToList(),
|
||||
Summary = new OptSummaryDto
|
||||
{
|
||||
TotalInStockBins = result.Summary.TotalInStockBins,
|
||||
TotalToBePurchasedBins = result.Summary.TotalToBePurchasedBins,
|
||||
TotalPieces = result.Summary.TotalPieces,
|
||||
TotalMaterialFormatted = result.Summary.TotalMaterialFormatted,
|
||||
TotalUsedFormatted = result.Summary.TotalUsedFormatted,
|
||||
TotalWasteFormatted = result.Summary.TotalWasteFormatted,
|
||||
Efficiency = result.Summary.Efficiency,
|
||||
TotalItemsNotPlaced = result.Summary.TotalItemsNotPlaced
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new OptimizeJobResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cutting Tools
|
||||
|
||||
[McpServerTool(Name = "list_cutting_tools"), Description("Lists all available cutting tools with their kerf (blade width) values.")]
|
||||
public async Task<CuttingToolListResult> ListCuttingTools(
|
||||
[Description("Include inactive tools (default false)")]
|
||||
bool includeInactive = false)
|
||||
{
|
||||
var tools = await _api.GetCuttingToolsAsync(includeInactive);
|
||||
|
||||
return new CuttingToolListResult
|
||||
{
|
||||
Success = true,
|
||||
Tools = tools.Select(t => new CuttingToolSummaryDto
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
KerfInches = t.KerfInches,
|
||||
IsDefault = t.IsDefault,
|
||||
IsActive = t.IsActive
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mapping Helpers
|
||||
|
||||
private static JobDetailDto MapJobDetail(ApiJobDetailDto j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingToolName,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.PartCount,
|
||||
StockCount = j.StockCount,
|
||||
Parts = j.Parts.Select(MapPart).ToList(),
|
||||
Stock = j.Stock.Select(MapStock).ToList()
|
||||
};
|
||||
|
||||
private static JobPartSummaryDto MapPart(ApiJobPartDto p) => new()
|
||||
{
|
||||
Id = p.Id,
|
||||
MaterialId = p.MaterialId,
|
||||
MaterialName = p.MaterialName,
|
||||
Name = p.Name,
|
||||
LengthInches = p.LengthInches,
|
||||
LengthFormatted = p.LengthFormatted,
|
||||
Quantity = p.Quantity
|
||||
};
|
||||
|
||||
private static JobStockSummaryDto MapStock(ApiJobStockDto s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.MaterialName,
|
||||
StockItemId = s.StockItemId,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = s.LengthFormatted,
|
||||
Quantity = s.Quantity,
|
||||
IsCustomLength = s.IsCustomLength,
|
||||
Priority = s.Priority
|
||||
};
|
||||
|
||||
private static OptBinDto MapBin(ApiPackedBinDto b) => new()
|
||||
{
|
||||
LengthFormatted = b.LengthFormatted,
|
||||
UsedFormatted = b.UsedFormatted,
|
||||
WasteFormatted = b.WasteFormatted,
|
||||
Efficiency = Math.Round(b.Efficiency, 1),
|
||||
Items = b.Items.Select(i => new OptItemDto
|
||||
{
|
||||
Name = i.Name,
|
||||
LengthInches = i.LengthInches,
|
||||
LengthFormatted = i.LengthFormatted
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
private static OptMaterialSummaryDto MapMaterialSummary(ApiMaterialPackingSummaryDto s) => new()
|
||||
{
|
||||
InStockBins = s.InStockBins,
|
||||
ToBePurchasedBins = s.ToBePurchasedBins,
|
||||
TotalPieces = s.TotalPieces,
|
||||
Efficiency = Math.Round(s.Efficiency, 1),
|
||||
ItemsNotPlaced = s.ItemsNotPlaced
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Job Tool DTOs
|
||||
|
||||
public class PartEntry
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class SimpleResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
public class JobSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
}
|
||||
|
||||
public class JobDetailDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
public List<JobPartSummaryDto> Parts { get; set; } = new();
|
||||
public List<JobStockSummaryDto> Stock { get; set; } = new();
|
||||
}
|
||||
|
||||
public class JobPartSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
public class JobStockSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int? StockItemId { get; set; }
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
public class JobListResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<JobSummaryDto> Jobs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class JobDetailResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobDetailDto? Job { get; set; }
|
||||
}
|
||||
|
||||
public class JobPartResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobPartSummaryDto? Part { get; set; }
|
||||
}
|
||||
|
||||
public class JobStockResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobStockSummaryDto? Stock { get; set; }
|
||||
}
|
||||
|
||||
public class CuttingToolSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CuttingToolListResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<CuttingToolSummaryDto> Tools { get; set; } = new();
|
||||
}
|
||||
|
||||
// Optimization result DTOs — streamlined for LLM consumption
|
||||
public class OptimizeJobResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<OptMaterialResultDto> Materials { get; set; } = new();
|
||||
public OptSummaryDto? Summary { get; set; }
|
||||
}
|
||||
|
||||
public class OptMaterialResultDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public List<OptBinDto> InStockBins { get; set; } = new();
|
||||
public List<OptBinDto> ToBePurchasedBins { get; set; } = new();
|
||||
public List<OptItemDto> ItemsNotPlaced { get; set; } = new();
|
||||
public OptMaterialSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OptBinDto
|
||||
{
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public string UsedFormatted { get; set; } = string.Empty;
|
||||
public string WasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public List<OptItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OptItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class OptSummaryDto
|
||||
{
|
||||
public int TotalInStockBins { get; set; }
|
||||
public int TotalToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public string TotalMaterialFormatted { get; set; } = string.Empty;
|
||||
public string TotalUsedFormatted { get; set; } = string.Empty;
|
||||
public string TotalWasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public int TotalItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
public class OptMaterialSummaryDto
|
||||
{
|
||||
public int InStockBins { get; set; }
|
||||
public int ToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1,14 +1,15 @@
|
||||
using CutList.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CutList.Mcp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Add DbContext for inventory tools
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
|
||||
// Register HttpClient for API calls to CutList.Web
|
||||
builder.Services.AddHttpClient<ApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri("http://localhost:5009");
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddMcpServer()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="css/report.css" />
|
||||
<link rel="stylesheet" href="CutList.Web.styles.css" />
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<span class="bi bi-boxes-nav-menu" aria-hidden="true"></span> Stock Items
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="orders">
|
||||
<span class="bi bi-cart-nav-menu" aria-hidden="true"></span> Orders
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="suppliers">
|
||||
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Suppliers
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-boxes' viewBox='0 0 16 16'%3E%3Cpath d='M7.752.066a.5.5 0 0 1 .496 0l3.75 2.143a.5.5 0 0 1 .252.434v3.995l3.498 2A.5.5 0 0 1 16 9.07v4.286a.5.5 0 0 1-.252.434l-3.75 2.143a.5.5 0 0 1-.496 0l-3.502-2-3.502 2.001a.5.5 0 0 1-.496 0l-3.75-2.143A.5.5 0 0 1 0 13.357V9.071a.5.5 0 0 1 .252-.434L3.75 6.638V2.643a.5.5 0 0 1 .252-.434zM4.25 7.504 1.508 9.071l2.742 1.567 2.742-1.567zM7.5 9.933l-2.75 1.571v3.134l2.75-1.571zm1 3.134 2.75 1.571v-3.134L8.5 9.933zm.508-3.996 2.742 1.567 2.742-1.567-2.742-1.567zm2.242-2.433V3.504L8.5 5.076V8.21zM7.5 8.21V5.076L4.75 3.504v3.134zM5.258 2.643 8 4.21l2.742-1.567L8 1.076zM15 9.933l-2.75 1.571v3.134L15 13.067zM3.75 14.638v-3.134L1 9.933v3.134z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-cart-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-cart3' viewBox='0 0 16 16'%3E%3Cpath d='M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l.84 4.479 9.144-.459L13.89 4H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-building-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-building' viewBox='0 0 16 16'%3E%3Cpath d='M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Z'/%3E%3Cpath d='M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1Zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3V1Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user