Compare commits
12 Commits
2a94ad63cb
...
c23c92e852
| Author | SHA1 | Date | |
|---|---|---|---|
| c23c92e852 | |||
| 2586f99c63 | |||
| 5f4e36c688 | |||
| ed705625e9 | |||
| 1ccdeb6817 | |||
| 69b282aaf3 | |||
| f932e8ba13 | |||
| 141176cc5d | |||
| 3fd354aff0 | |||
| b603a4b3e7 | |||
| 5468b2748d | |||
| 8ed10939d4 |
@@ -1,13 +1,22 @@
|
|||||||
# CLAUDE.md
|
# 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.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
## 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)
|
The solution contains three projects:
|
||||||
**Key Dependencies**: Math-Expression-Evaluator (input parsing), Newtonsoft.Json (serialization)
|
|
||||||
|
| 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
|
## Build Commands
|
||||||
|
|
||||||
@@ -15,12 +24,18 @@ CutList is a Windows Forms 1D bin packing optimization application that helps us
|
|||||||
# Build entire solution
|
# Build entire solution
|
||||||
dotnet build CutList.sln
|
dotnet build CutList.sln
|
||||||
|
|
||||||
# Build specific project
|
# Build specific projects
|
||||||
dotnet build CutList/CutList.csproj
|
dotnet build CutList/CutList.csproj
|
||||||
dotnet build CutList.Core/CutList.Core.csproj
|
dotnet build CutList.Core/CutList.Core.csproj
|
||||||
|
dotnet build CutList.Web/CutList.Web.csproj
|
||||||
|
|
||||||
# Run the application
|
# Run applications
|
||||||
dotnet run --project CutList/CutList.csproj
|
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
|
# Clean build
|
||||||
dotnet clean CutList.sln
|
dotnet clean CutList.sln
|
||||||
@@ -28,69 +43,179 @@ dotnet clean CutList.sln
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Project Structure
|
### CutList.Core — Domain & Algorithms
|
||||||
|
|
||||||
- **CutList/** - Main Windows Forms application (UI layer)
|
**Key Domain Models**:
|
||||||
- **CutList.Core/** - Core library with domain models and packing algorithms (shareable, platform-agnostic)
|
- **BinItem**: Item to be packed (label, length)
|
||||||
|
|
||||||
### 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)
|
|
||||||
- **MultiBin**: Stock bin type with length, quantity (-1 = unlimited), priority
|
- **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
|
- **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.)
|
**Unit Handling**:
|
||||||
- **FormatHelper**: Converts decimals to mixed fractions for display
|
- `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
|
- 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
|
### CutList (WinForms) — Desktop UI
|
||||||
- **Collections are encapsulated** - use AsReadOnly(), access via Add* methods
|
|
||||||
- **Validation in domain models** - constructors and properties validate inputs
|
- MVP pattern: `MainForm` implements `IMainView`, `MainFormPresenter` orchestrates logic
|
||||||
- **Parameterless constructors** on Tool/MultiBin are for JSON serialization only
|
- `Document` holds application state
|
||||||
- **Spacing property** on engines handles blade/kerf width
|
- `CutListService` bridges UI models to core packing algorithms
|
||||||
- **Priority system**: Lower priority bins are used first
|
- `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 (TPH Inheritance)
|
||||||
|
Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensions`, `FlatBarDimensions`, `SquareBarDimensions`, `SquareTubeDimensions`, `RectangularTubeDimensions`, `AngleDimensions`, `ChannelDimensions`, `IBeamDimensions`, `PipeDimensions`. 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
|
||||||
|
- **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)
|
||||||
|
- Stock: `AddStockAsync`, `UpdateStockAsync`, `DeleteStockAsync`
|
||||||
|
- 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 %
|
||||||
|
|
||||||
|
### 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); locked jobs show banner + disable editing |
|
||||||
|
| `/jobs/{Id}/results` | Jobs/Results | Optimization results, summary cards, "Add to Order List" (locks job), Print Report |
|
||||||
|
| `/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
|
||||||
|
- **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
|
## Key Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `Presenters/MainFormPresenter.cs` | Main business logic orchestrator |
|
| `CutList.Core/Nesting/AdvancedFitEngine.cs` | Core 1D bin packing algorithm |
|
||||||
| `Services/CutListService.cs` | Packing algorithm interface |
|
| `CutList.Core/Nesting/MultiBinEngine.cs` | Multi-bin type orchestration |
|
||||||
| `CutList.Core/Nesting/AdvancedFitEngine.cs` | Core packing algorithm |
|
| `CutList.Core/ArchUnits.cs` | Architectural unit parsing/conversion |
|
||||||
| `CutList.Core/Nesting/MultiBinEngine.cs` | Multi-bin orchestration |
|
| `CutList.Core/Formatting/FormatHelper.cs` | Display formatting |
|
||||||
| `CutList.Core/ArchUnits.cs` | Unit conversion |
|
| `CutList.Web/Data/ApplicationDbContext.cs` | EF Core context with all DbSets and configuration |
|
||||||
| `CutList.Core/FormatHelper.cs` | Output formatting |
|
| `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) |
|
||||||
|
| `CutList.Web/Components/Pages/Jobs/Results.razor` | Optimization results + order creation |
|
||||||
|
| `CutList/Presenters/MainFormPresenter.cs` | WinForms business logic orchestrator |
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
<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/app.css" />
|
||||||
<link rel="stylesheet" href="css/report.css" />
|
<link rel="stylesheet" href="css/report.css" />
|
||||||
<link rel="stylesheet" href="CutList.Web.styles.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
|
<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">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="suppliers">
|
<NavLink class="nav-link" href="suppliers">
|
||||||
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> 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");
|
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 {
|
.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");
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,19 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!IsNew && job.IsLocked)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<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.
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-warning btn-sm" @onclick="UnlockJob">
|
||||||
|
<i class="bi bi-unlock"></i> Unlock Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (loading)
|
@if (loading)
|
||||||
{
|
{
|
||||||
<p><em>Loading...</em></p>
|
<p><em>Loading...</em></p>
|
||||||
@@ -286,6 +299,15 @@ else
|
|||||||
|
|
||||||
private bool IsNew => !Id.HasValue;
|
private bool IsNew => !Id.HasValue;
|
||||||
|
|
||||||
|
private async Task UnlockJob()
|
||||||
|
{
|
||||||
|
if (Id.HasValue)
|
||||||
|
{
|
||||||
|
await JobService.UnlockAsync(Id.Value);
|
||||||
|
job = (await JobService.GetByIdAsync(Id.Value))!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
materials = await MaterialService.GetAllAsync();
|
materials = await MaterialService.GetAllAsync();
|
||||||
@@ -322,6 +344,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<EditForm Model="job" OnValidSubmit="SaveJobAsync">
|
<EditForm Model="job" OnValidSubmit="SaveJobAsync">
|
||||||
|
<fieldset disabled="@job.IsLocked">
|
||||||
@if (!IsNew)
|
@if (!IsNew)
|
||||||
{
|
{
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -371,6 +394,7 @@ else
|
|||||||
</button>
|
</button>
|
||||||
<a href="jobs" class="btn btn-outline-secondary">Back</a>
|
<a href="jobs" class="btn btn-outline-secondary">Back</a>
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
</EditForm>
|
</EditForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,7 +405,10 @@ else
|
|||||||
<div class="card">
|
<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">Parts to Cut</h5>
|
<h5 class="mb-0">Parts to Cut</h5>
|
||||||
|
@if (!job.IsLocked)
|
||||||
|
{
|
||||||
<button class="btn btn-primary" @onclick="ShowAddPartForm">Add Part</button>
|
<button class="btn btn-primary" @onclick="ShowAddPartForm">Add Part</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (job.Parts.Count == 0)
|
@if (job.Parts.Count == 0)
|
||||||
@@ -413,8 +440,11 @@ else
|
|||||||
<td>@part.Quantity</td>
|
<td>@part.Quantity</td>
|
||||||
<td>@(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)</td>
|
<td>@(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditPart(part)">Edit</button>
|
@if (!job.IsLocked)
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeletePart(part)">Delete</button>
|
{
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditPart(part)" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeletePart(part)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -545,6 +575,8 @@ else
|
|||||||
<div class="card">
|
<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">Stock for This Job</h5>
|
<h5 class="mb-0">Stock for This Job</h5>
|
||||||
|
@if (!job.IsLocked)
|
||||||
|
{
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button class="btn btn-success" @onclick="ShowImportModal" disabled="@(job.Parts.Count == 0)"
|
<button class="btn btn-success" @onclick="ShowImportModal" disabled="@(job.Parts.Count == 0)"
|
||||||
title="@(job.Parts.Count == 0 ? "Add parts first to match against inventory" : "Find and import stock matching your parts")">
|
title="@(job.Parts.Count == 0 ? "Add parts first to match against inventory" : "Find and import stock matching your parts")">
|
||||||
@@ -555,6 +587,7 @@ else
|
|||||||
<button class="btn btn-outline-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
|
<button class="btn btn-outline-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (showStockForm)
|
@if (showStockForm)
|
||||||
@@ -731,8 +764,11 @@ else
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditStock(stock)">Edit</button>
|
@if (!job.IsLocked)
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteStock(stock)">Delete</button>
|
{
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditStock(stock)" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteStock(stock)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,28 +43,36 @@ else
|
|||||||
<th>Customer</th>
|
<th>Customer</th>
|
||||||
<th>Cutting Tool</th>
|
<th>Cutting Tool</th>
|
||||||
<th>Last Modified</th>
|
<th>Last Modified</th>
|
||||||
<th style="width: 200px;">Actions</th>
|
<th style="width: 150px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var job in jobs)
|
@foreach (var job in pagedJobs)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="jobs/@job.Id">@job.JobNumber</a></td>
|
<td>
|
||||||
|
<a href="jobs/@job.Id">@job.JobNumber</a>
|
||||||
|
@if (job.IsLocked)
|
||||||
|
{
|
||||||
|
<i class="bi bi-lock-fill text-warning ms-1" title="Locked — materials ordered"></i>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>@(job.Name ?? "-")</td>
|
<td>@(job.Name ?? "-")</td>
|
||||||
<td>@(job.Customer ?? "-")</td>
|
<td>@(job.Customer ?? "-")</td>
|
||||||
<td>@(job.CuttingTool?.Name ?? "-")</td>
|
<td>@(job.CuttingTool?.Name ?? "-")</td>
|
||||||
<td>@((job.UpdatedAt ?? job.CreatedAt).ToLocalTime().ToString("g"))</td>
|
<td>@((job.UpdatedAt ?? job.CreatedAt).ToLocalTime().ToString("g"))</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="jobs/@job.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
<a href="jobs/@job.Id" class="btn btn-sm btn-outline-primary" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||||
<a href="jobs/@job.Id/results" class="btn btn-sm btn-success">Optimize</a>
|
<a href="jobs/@job.Id/results" class="btn btn-sm btn-success" title="Optimize"><i class="bi bi-scissors"></i></a>
|
||||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateJob(job)">Copy</button>
|
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateJob(job)" title="Copy"><i class="bi bi-copy"></i></button>
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(job)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(job)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="jobs.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteDialog"
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
@@ -77,10 +85,14 @@ else
|
|||||||
private List<Job> jobs = new();
|
private List<Job> jobs = new();
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
private bool creating = false;
|
private bool creating = false;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
private ConfirmDialog deleteDialog = null!;
|
private ConfirmDialog deleteDialog = null!;
|
||||||
private Job? jobToDelete;
|
private Job? jobToDelete;
|
||||||
private string deleteMessage = "";
|
private string deleteMessage = "";
|
||||||
|
|
||||||
|
private IEnumerable<Job> pagedJobs => jobs.Skip((currentPage - 1) * pageSize).Take(pageSize);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
jobs = await JobService.GetAllAsync();
|
jobs = await JobService.GetAllAsync();
|
||||||
@@ -114,9 +126,15 @@ else
|
|||||||
{
|
{
|
||||||
await JobService.DeleteAsync(jobToDelete.Id);
|
await JobService.DeleteAsync(jobToDelete.Id);
|
||||||
jobs = await JobService.GetAllAsync();
|
jobs = await JobService.GetAllAsync();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)jobs.Count / pageSize);
|
||||||
|
if (currentPage > totalPages && totalPages > 0)
|
||||||
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPageChanged(int page) => currentPage = page;
|
||||||
|
|
||||||
private async Task DuplicateJob(Job job)
|
private async Task DuplicateJob(Job job)
|
||||||
{
|
{
|
||||||
var duplicate = await JobService.DuplicateAsync(job.Id);
|
var duplicate = await JobService.DuplicateAsync(job.Id);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
@inject CutListPackingService PackingService
|
@inject CutListPackingService PackingService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@inject PurchaseItemService PurchaseItemService
|
||||||
|
@inject StockItemService StockItemService
|
||||||
@using CutList.Core
|
@using CutList.Core
|
||||||
@using CutList.Core.Nesting
|
@using CutList.Core.Nesting
|
||||||
@using CutList.Core.Formatting
|
@using CutList.Core.Formatting
|
||||||
@@ -21,7 +23,13 @@ else
|
|||||||
{
|
{
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1>@job.DisplayName</h1>
|
<h1>
|
||||||
|
@job.DisplayName
|
||||||
|
@if (job.IsLocked)
|
||||||
|
{
|
||||||
|
<i class="bi bi-lock-fill text-warning ms-2" title="Job locked — materials ordered"></i>
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
@if (!string.IsNullOrWhiteSpace(job.Customer))
|
@if (!string.IsNullOrWhiteSpace(job.Customer))
|
||||||
{
|
{
|
||||||
<p class="text-muted mb-0">Customer: @job.Customer</p>
|
<p class="text-muted mb-0">Customer: @job.Customer</p>
|
||||||
@@ -60,7 +68,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Overall Summary Cards -->
|
<!-- Overall Summary Cards -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4 print-summary">
|
||||||
<div class="col-md-3 col-6 mb-3">
|
<div class="col-md-3 col-6 mb-3">
|
||||||
<div class="card text-center">
|
<div class="card text-center">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -96,7 +104,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stock Summary -->
|
<!-- Stock Summary -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4 print-stock-summary">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<div class="card border-success">
|
<div class="card border-success">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="card-header bg-success text-white">
|
||||||
@@ -115,7 +123,29 @@ else
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3>@summary.TotalToBePurchasedBins bars</h3>
|
<h3>@summary.TotalToBePurchasedBins bars</h3>
|
||||||
|
@if (summary.TotalToBePurchasedBins > 0)
|
||||||
|
{
|
||||||
|
@if (addedToOrderList)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success mb-0 mt-2 py-2">
|
||||||
|
Added to order list. <a href="orders">View Orders</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button class="btn btn-warning btn-sm mt-2" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
||||||
|
@if (addingToOrderList)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
<i class="bi bi-cart-plus"></i> Add to Order List
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<p class="text-muted mb-0">Need to order from supplier</p>
|
<p class="text-muted mb-0">Need to order from supplier</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,6 +225,9 @@ else
|
|||||||
private MultiMaterialPackingSummary? summary;
|
private MultiMaterialPackingSummary? summary;
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
|
||||||
|
private bool addingToOrderList;
|
||||||
|
private bool addedToOrderList;
|
||||||
|
|
||||||
private bool CanOptimize => job != null &&
|
private bool CanOptimize => job != null &&
|
||||||
job.Parts.Count > 0 &&
|
job.Parts.Count > 0 &&
|
||||||
job.CuttingToolId != null;
|
job.CuttingToolId != null;
|
||||||
@@ -209,6 +242,7 @@ else
|
|||||||
// Pass job stock if configured, otherwise packing service uses all available stock
|
// Pass job stock if configured, otherwise packing service uses all available stock
|
||||||
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
||||||
summary = PackingService.GetSummary(packResult);
|
summary = PackingService.GetSummary(packResult);
|
||||||
|
addedToOrderList = job.IsLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -250,6 +284,58 @@ else
|
|||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private async Task AddToOrderList()
|
||||||
|
{
|
||||||
|
addingToOrderList = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var purchaseItems = new List<PurchaseItem>();
|
||||||
|
var stockItems = await StockItemService.GetAllAsync();
|
||||||
|
|
||||||
|
foreach (var materialResult in packResult!.MaterialResults)
|
||||||
|
{
|
||||||
|
if (materialResult.ToBePurchasedBins.Count == 0) continue;
|
||||||
|
|
||||||
|
var materialId = materialResult.Material.Id;
|
||||||
|
|
||||||
|
// Group bins by length to consolidate quantities
|
||||||
|
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
|
||||||
|
{
|
||||||
|
var lengthInches = (decimal)group.Key;
|
||||||
|
var quantity = group.Count();
|
||||||
|
|
||||||
|
// Find the matching stock item
|
||||||
|
var stockItem = stockItems.FirstOrDefault(s =>
|
||||||
|
s.MaterialId == materialId && s.LengthInches == lengthInches);
|
||||||
|
|
||||||
|
if (stockItem != null)
|
||||||
|
{
|
||||||
|
purchaseItems.Add(new PurchaseItem
|
||||||
|
{
|
||||||
|
StockItemId = stockItem.Id,
|
||||||
|
Quantity = quantity,
|
||||||
|
JobId = Id,
|
||||||
|
Status = PurchaseItemStatus.Pending
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseItems.Count > 0)
|
||||||
|
{
|
||||||
|
await PurchaseItemService.CreateBulkAsync(purchaseItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JobService.LockAsync(Id);
|
||||||
|
job = await JobService.GetByIdAsync(Id);
|
||||||
|
addedToOrderList = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
addingToOrderList = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PrintReport()
|
private async Task PrintReport()
|
||||||
{
|
{
|
||||||
var filename = $"CutList - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
var filename = $"CutList - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ else if (materials.Count == 0)
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
<MaterialFilter AvailableGrades="availableGrades" Value="filterState" ValueChanged="OnFilterChanged" />
|
||||||
|
|
||||||
|
@if (filteredMaterials.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
No materials match your filters.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -40,29 +50,30 @@ else
|
|||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Grade</th>
|
<th>Grade</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Description</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
<th style="width: 160px;">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var material in materials)
|
@foreach (var material in pagedMaterials)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@material.Shape.GetDisplayName()</td>
|
<td>@material.Shape.GetDisplayName()</td>
|
||||||
<td>@material.Type</td>
|
<td>@material.Type</td>
|
||||||
<td>@material.Grade</td>
|
<td>@material.Grade</td>
|
||||||
<td>@material.Size</td>
|
<td>@material.Size</td>
|
||||||
<td>@material.Description</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex gap-1">
|
<div class="d-flex gap-1">
|
||||||
<a href="materials/@material.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
<a href="materials/@material.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(material)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(material)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="filteredMaterials.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteDialog"
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
@@ -75,9 +86,42 @@ else
|
|||||||
private List<Material> materials = new();
|
private List<Material> materials = new();
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
private ConfirmDialog deleteDialog = null!;
|
private ConfirmDialog deleteDialog = null!;
|
||||||
private Material? materialToDelete;
|
private Material? materialToDelete;
|
||||||
private string deleteMessage = "";
|
private string deleteMessage = "";
|
||||||
|
private MaterialFilterState filterState = new();
|
||||||
|
|
||||||
|
private List<Material> filteredMaterials => materials.Where(m =>
|
||||||
|
{
|
||||||
|
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.Description, search)
|
||||||
|
&& !Contains(m.Shape.GetDisplayName(), search))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
private IEnumerable<string> availableGrades => materials
|
||||||
|
.Select(m => m.Grade)
|
||||||
|
.Where(g => !string.IsNullOrEmpty(g))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(g => g)!;
|
||||||
|
|
||||||
|
private IEnumerable<Material> pagedMaterials => filteredMaterials
|
||||||
|
.Skip((currentPage - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -95,6 +139,12 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnFilterChanged(MaterialFilterState state)
|
||||||
|
{
|
||||||
|
filterState = state;
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void ConfirmDelete(Material material)
|
private void ConfirmDelete(Material material)
|
||||||
{
|
{
|
||||||
materialToDelete = material;
|
materialToDelete = material;
|
||||||
@@ -108,6 +158,15 @@ else
|
|||||||
{
|
{
|
||||||
await MaterialService.DeleteAsync(materialToDelete.Id);
|
await MaterialService.DeleteAsync(materialToDelete.Id);
|
||||||
materials = await MaterialService.GetAllAsync();
|
materials = await MaterialService.GetAllAsync();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)filteredMaterials.Count / pageSize);
|
||||||
|
if (currentPage > totalPages && totalPages > 0)
|
||||||
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPageChanged(int page) => currentPage = page;
|
||||||
|
|
||||||
|
private static bool Contains(string? value, string search) =>
|
||||||
|
value != null && value.Contains(search, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
@page "/orders/add"
|
||||||
|
@inject PurchaseItemService PurchaseItemService
|
||||||
|
@inject StockItemService StockItemService
|
||||||
|
@inject SupplierService SupplierService
|
||||||
|
@inject JobService JobService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@using CutList.Core.Formatting
|
||||||
|
@using CutList.Web.Data.Entities
|
||||||
|
|
||||||
|
<PageTitle>Add Order Item</PageTitle>
|
||||||
|
|
||||||
|
<h1>Add Order Item</h1>
|
||||||
|
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Order Item Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<EditForm Model="item" OnValidSubmit="SaveAsync">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Stock Item</label>
|
||||||
|
<select class="form-select" @bind="item.StockItemId">
|
||||||
|
<option value="0">-- Select Stock Item --</option>
|
||||||
|
@foreach (var group in stockItemGroups)
|
||||||
|
{
|
||||||
|
<optgroup label="@group.Key">
|
||||||
|
@foreach (var si in group.Value)
|
||||||
|
{
|
||||||
|
<option value="@si.Id">@si.Material.Size - @ArchUnits.FormatFromInches((double)si.LengthInches)</option>
|
||||||
|
}
|
||||||
|
</optgroup>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Quantity</label>
|
||||||
|
<InputNumber class="form-control" @bind-Value="item.Quantity" min="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Supplier (optional)</label>
|
||||||
|
<select class="form-select" @bind="item.SupplierId">
|
||||||
|
<option value="">-- Select Supplier --</option>
|
||||||
|
@foreach (var supplier in suppliers)
|
||||||
|
{
|
||||||
|
<option value="@supplier.Id">@supplier.Name</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Job (optional)</label>
|
||||||
|
<select class="form-select" @bind="item.JobId">
|
||||||
|
<option value="">-- Select Job --</option>
|
||||||
|
@foreach (var job in jobs)
|
||||||
|
{
|
||||||
|
<option value="@job.Id">@job.DisplayName</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Notes (optional)</label>
|
||||||
|
<InputText class="form-control" @bind-Value="item.Notes" placeholder="Any notes about this order" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(errorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@errorMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary" disabled="@saving">
|
||||||
|
@if (saving)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
Add to Order List
|
||||||
|
</button>
|
||||||
|
<a href="orders" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private PurchaseItem item = new() { Quantity = 1 };
|
||||||
|
private List<StockItem> stockItems = new();
|
||||||
|
private Dictionary<string, List<StockItem>> stockItemGroups = new();
|
||||||
|
private List<Supplier> suppliers = new();
|
||||||
|
private List<Job> jobs = new();
|
||||||
|
private bool loading = true;
|
||||||
|
private bool saving;
|
||||||
|
private string? errorMessage;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
stockItems = await StockItemService.GetAllAsync();
|
||||||
|
suppliers = await SupplierService.GetAllAsync();
|
||||||
|
jobs = await JobService.GetAllAsync();
|
||||||
|
|
||||||
|
stockItemGroups = stockItems
|
||||||
|
.GroupBy(s => s.Material.Shape.GetDisplayName())
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.ToDictionary(g => g.Key, g => g.OrderBy(s => s.Material.Size).ThenBy(s => s.LengthInches).ToList());
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAsync()
|
||||||
|
{
|
||||||
|
errorMessage = null;
|
||||||
|
saving = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (item.StockItemId == 0)
|
||||||
|
{
|
||||||
|
errorMessage = "Please select a stock item";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Quantity <= 0)
|
||||||
|
{
|
||||||
|
errorMessage = "Quantity must be at least 1";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PurchaseItemService.CreateAsync(item);
|
||||||
|
Navigation.NavigateTo("orders");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
@page "/orders"
|
||||||
|
@inject PurchaseItemService PurchaseItemService
|
||||||
|
@inject SupplierService SupplierService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@using CutList.Core.Formatting
|
||||||
|
@using CutList.Web.Data.Entities
|
||||||
|
|
||||||
|
<PageTitle>Orders</PageTitle>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1>To Be Ordered</h1>
|
||||||
|
<a href="orders/add" class="btn btn-primary">Add Item</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Track material that needs to be ordered from suppliers. Items are added manually or automatically from job optimization results.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ul class="nav nav-tabs mb-3">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link @(activeTab == "pending" ? "active" : "")" @onclick='() => SetTab("pending")'>
|
||||||
|
Pending
|
||||||
|
@if (pendingCount > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark ms-1">@pendingCount</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link @(activeTab == "ordered" ? "active" : "")" @onclick='() => SetTab("ordered")'>
|
||||||
|
Ordered
|
||||||
|
@if (orderedCount > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary ms-1">@orderedCount</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link @(activeTab == "all" ? "active" : "")" @onclick='() => SetTab("all")'>All</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if (tabItems.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
@if (activeTab == "pending")
|
||||||
|
{
|
||||||
|
<span>No pending items. <a href="orders/add">Add an item</a> or use "Add to Order List" from a job's results page.</span>
|
||||||
|
}
|
||||||
|
else if (activeTab == "ordered")
|
||||||
|
{
|
||||||
|
<span>No ordered items.</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>No order items found. <a href="orders/add">Add your first item</a>.</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MaterialFilter AvailableGrades="availableGrades" Value="filterState" ValueChanged="OnFilterChanged" />
|
||||||
|
|
||||||
|
@if (filteredItems.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
No items match your filters.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Material</th>
|
||||||
|
<th>Length</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Supplier</th>
|
||||||
|
<th>Job</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th style="width: 140px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in pagedItems)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@item.StockItem.Material.DisplayName</td>
|
||||||
|
<td>@ArchUnits.FormatFromInches((double)item.StockItem.LengthInches)</td>
|
||||||
|
<td>@item.Quantity</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-select form-select-sm" style="min-width: 140px;"
|
||||||
|
value="@(item.SupplierId?.ToString() ?? "")"
|
||||||
|
@onchange="(e) => OnSupplierChanged(item, e)">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@foreach (var supplier in suppliers)
|
||||||
|
{
|
||||||
|
<option value="@supplier.Id">@supplier.Name</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (item.Job != null)
|
||||||
|
{
|
||||||
|
<a href="jobs/@item.Job.Id">@item.Job.DisplayName</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge @GetStatusBadgeClass(item.Status)">@item.Status</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-muted small">@(item.Notes ?? "-")</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
@if (item.Status == PurchaseItemStatus.Pending)
|
||||||
|
{
|
||||||
|
<button class="btn btn-sm btn-outline-primary" @onclick="() => MarkOrdered(item)" title="Mark Ordered">
|
||||||
|
<i class="bi bi-truck"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (item.Status == PurchaseItemStatus.Ordered)
|
||||||
|
{
|
||||||
|
<button class="btn btn-sm btn-outline-success" @onclick="() => MarkReceived(item)" title="Mark Received">
|
||||||
|
<i class="bi bi-check-lg"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(item)" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="filteredItems.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
|
Title="Delete Order Item"
|
||||||
|
Message="@deleteMessage"
|
||||||
|
ConfirmText="Delete"
|
||||||
|
OnConfirm="DeleteConfirmed" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<PurchaseItem> allItems = new();
|
||||||
|
private List<Supplier> suppliers = new();
|
||||||
|
private bool loading = true;
|
||||||
|
private string activeTab = "pending";
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
|
private ConfirmDialog deleteDialog = null!;
|
||||||
|
private PurchaseItem? itemToDelete;
|
||||||
|
private string deleteMessage = "";
|
||||||
|
private MaterialFilterState filterState = new();
|
||||||
|
|
||||||
|
private int pendingCount => allItems.Count(i => i.Status == PurchaseItemStatus.Pending);
|
||||||
|
private int orderedCount => allItems.Count(i => i.Status == PurchaseItemStatus.Ordered);
|
||||||
|
|
||||||
|
private List<PurchaseItem> tabItems => activeTab switch
|
||||||
|
{
|
||||||
|
"pending" => allItems.Where(i => i.Status == PurchaseItemStatus.Pending).ToList(),
|
||||||
|
"ordered" => allItems.Where(i => i.Status == PurchaseItemStatus.Ordered).ToList(),
|
||||||
|
_ => allItems
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<PurchaseItem> filteredItems => tabItems.Where(i =>
|
||||||
|
{
|
||||||
|
var m = i.StockItem.Material;
|
||||||
|
if (filterState.Shape.HasValue && m.Shape != filterState.Shape.Value)
|
||||||
|
return false;
|
||||||
|
if (filterState.Type.HasValue && m.Type != filterState.Type.Value)
|
||||||
|
return false;
|
||||||
|
if (!string.IsNullOrEmpty(filterState.Grade) && m.Grade != filterState.Grade)
|
||||||
|
return false;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filterState.SearchText))
|
||||||
|
{
|
||||||
|
var search = filterState.SearchText.Trim();
|
||||||
|
if (!Contains(m.Size, search)
|
||||||
|
&& !Contains(m.Grade, search)
|
||||||
|
&& !Contains(m.Shape.GetDisplayName(), search)
|
||||||
|
&& !Contains(i.Notes, search)
|
||||||
|
&& !Contains(i.Job?.DisplayName, search)
|
||||||
|
&& !Contains(i.Supplier?.Name, search))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
private IEnumerable<string> availableGrades => tabItems
|
||||||
|
.Select(i => i.StockItem.Material.Grade)
|
||||||
|
.Where(g => !string.IsNullOrEmpty(g))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(g => g)!;
|
||||||
|
|
||||||
|
private IEnumerable<PurchaseItem> pagedItems => filteredItems
|
||||||
|
.Skip((currentPage - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
allItems = await PurchaseItemService.GetAllAsync();
|
||||||
|
suppliers = await SupplierService.GetAllAsync();
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTab(string tab)
|
||||||
|
{
|
||||||
|
activeTab = tab;
|
||||||
|
currentPage = 1;
|
||||||
|
filterState = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilterChanged(MaterialFilterState state)
|
||||||
|
{
|
||||||
|
filterState = state;
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSupplierChanged(PurchaseItem item, ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
int? supplierId = int.TryParse(e.Value?.ToString(), out var id) && id > 0 ? id : null;
|
||||||
|
await PurchaseItemService.UpdateSupplierAsync(item.Id, supplierId);
|
||||||
|
item.SupplierId = supplierId;
|
||||||
|
item.Supplier = supplierId.HasValue ? suppliers.FirstOrDefault(s => s.Id == supplierId.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MarkOrdered(PurchaseItem item)
|
||||||
|
{
|
||||||
|
await PurchaseItemService.UpdateStatusAsync(item.Id, PurchaseItemStatus.Ordered);
|
||||||
|
item.Status = PurchaseItemStatus.Ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MarkReceived(PurchaseItem item)
|
||||||
|
{
|
||||||
|
await PurchaseItemService.UpdateStatusAsync(item.Id, PurchaseItemStatus.Received);
|
||||||
|
allItems = await PurchaseItemService.GetAllAsync();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)filteredItems.Count / pageSize);
|
||||||
|
if (currentPage > totalPages && totalPages > 0)
|
||||||
|
currentPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmDelete(PurchaseItem item)
|
||||||
|
{
|
||||||
|
itemToDelete = item;
|
||||||
|
deleteMessage = $"Are you sure you want to delete this order item ({item.StockItem.Material.DisplayName} - {ArchUnits.FormatFromInches((double)item.StockItem.LengthInches)} x{item.Quantity})?";
|
||||||
|
deleteDialog.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteConfirmed()
|
||||||
|
{
|
||||||
|
if (itemToDelete != null)
|
||||||
|
{
|
||||||
|
await PurchaseItemService.DeleteAsync(itemToDelete.Id);
|
||||||
|
allItems = await PurchaseItemService.GetAllAsync();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)filteredItems.Count / pageSize);
|
||||||
|
if (currentPage > totalPages && totalPages > 0)
|
||||||
|
currentPage = totalPages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPageChanged(int page) => currentPage = page;
|
||||||
|
|
||||||
|
private static string GetStatusBadgeClass(PurchaseItemStatus status) => status switch
|
||||||
|
{
|
||||||
|
PurchaseItemStatus.Pending => "bg-warning text-dark",
|
||||||
|
PurchaseItemStatus.Ordered => "bg-primary",
|
||||||
|
PurchaseItemStatus.Received => "bg-success",
|
||||||
|
_ => "bg-secondary"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool Contains(string? value, string search) =>
|
||||||
|
value != null && value.Contains(search, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -264,8 +264,8 @@ else
|
|||||||
<td>@(offering.PartNumber ?? "-")</td>
|
<td>@(offering.PartNumber ?? "-")</td>
|
||||||
<td>@(offering.Price.HasValue ? offering.Price.Value.ToString("C") : "-")</td>
|
<td>@(offering.Price.HasValue ? offering.Price.Value.ToString("C") : "-")</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditOffering(offering)">Edit</button>
|
<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)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteOffering(offering)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ else if (stockItems.Count == 0)
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
<MaterialFilter AvailableGrades="availableGrades" Value="filterState" ValueChanged="OnFilterChanged" />
|
||||||
|
|
||||||
|
@if (filteredItems.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
No stock items match your filters.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -37,11 +47,11 @@ else
|
|||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Length</th>
|
<th>Length</th>
|
||||||
<th>On Hand</th>
|
<th>On Hand</th>
|
||||||
<th style="width: 160px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var item in stockItems)
|
@foreach (var item in pagedItems)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@item.Material.Shape.GetDisplayName()</td>
|
<td>@item.Material.Shape.GetDisplayName()</td>
|
||||||
@@ -61,14 +71,17 @@ else
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex gap-1">
|
<div class="d-flex gap-1">
|
||||||
<a href="stock/@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
<a href="stock/@item.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(item)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(item)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="filteredItems.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteDialog"
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
@@ -80,9 +93,45 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private List<StockItem> stockItems = new();
|
private List<StockItem> stockItems = new();
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
private ConfirmDialog deleteDialog = null!;
|
private ConfirmDialog deleteDialog = null!;
|
||||||
private StockItem? itemToDelete;
|
private StockItem? itemToDelete;
|
||||||
private string deleteMessage = "";
|
private string deleteMessage = "";
|
||||||
|
private MaterialFilterState filterState = new();
|
||||||
|
|
||||||
|
private List<StockItem> filteredItems => stockItems.Where(s =>
|
||||||
|
{
|
||||||
|
var m = s.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.Description, search)
|
||||||
|
&& !Contains(m.Shape.GetDisplayName(), search)
|
||||||
|
&& !Contains(s.Name, search)
|
||||||
|
&& !Contains(s.Notes, search))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
private IEnumerable<string> availableGrades => stockItems
|
||||||
|
.Select(s => s.Material.Grade)
|
||||||
|
.Where(g => !string.IsNullOrEmpty(g))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(g => g)!;
|
||||||
|
|
||||||
|
private IEnumerable<StockItem> pagedItems => filteredItems
|
||||||
|
.Skip((currentPage - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -90,6 +139,12 @@ else
|
|||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnFilterChanged(MaterialFilterState state)
|
||||||
|
{
|
||||||
|
filterState = state;
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void ConfirmDelete(StockItem item)
|
private void ConfirmDelete(StockItem item)
|
||||||
{
|
{
|
||||||
itemToDelete = item;
|
itemToDelete = item;
|
||||||
@@ -103,6 +158,15 @@ else
|
|||||||
{
|
{
|
||||||
await StockItemService.DeleteAsync(itemToDelete.Id);
|
await StockItemService.DeleteAsync(itemToDelete.Id);
|
||||||
stockItems = await StockItemService.GetAllAsync();
|
stockItems = await StockItemService.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 bool Contains(string? value, string search) =>
|
||||||
|
value != null && value.Contains(search, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ else
|
|||||||
<td>@(offering.PartNumber ?? "-")</td>
|
<td>@(offering.PartNumber ?? "-")</td>
|
||||||
<td>@(offering.Price.HasValue ? offering.Price.Value.ToString("C") : "-")</td>
|
<td>@(offering.Price.HasValue ? offering.Price.Value.ToString("C") : "-")</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditOffering(offering)">Edit</button>
|
<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)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteOffering(offering)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ else
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Contact Info</th>
|
<th>Contact Info</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
<th style="width: 160px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var supplier in suppliers)
|
@foreach (var supplier in pagedSuppliers)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="suppliers/@supplier.Id">@supplier.Name</a></td>
|
<td><a href="suppliers/@supplier.Id">@supplier.Name</a></td>
|
||||||
@@ -39,14 +39,16 @@ else
|
|||||||
<td>@TruncateText(supplier.Notes, 50)</td>
|
<td>@TruncateText(supplier.Notes, 50)</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex gap-1">
|
<div class="d-flex gap-1">
|
||||||
<a href="suppliers/@supplier.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
<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)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(supplier)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="suppliers.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteDialog"
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
@@ -58,10 +60,14 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private List<Supplier> suppliers = new();
|
private List<Supplier> suppliers = new();
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
private ConfirmDialog deleteDialog = null!;
|
private ConfirmDialog deleteDialog = null!;
|
||||||
private Supplier? supplierToDelete;
|
private Supplier? supplierToDelete;
|
||||||
private string deleteMessage = "";
|
private string deleteMessage = "";
|
||||||
|
|
||||||
|
private IEnumerable<Supplier> pagedSuppliers => suppliers.Skip((currentPage - 1) * pageSize).Take(pageSize);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
suppliers = await SupplierService.GetAllAsync();
|
suppliers = await SupplierService.GetAllAsync();
|
||||||
@@ -81,9 +87,15 @@ else
|
|||||||
{
|
{
|
||||||
await SupplierService.DeleteAsync(supplierToDelete.Id);
|
await SupplierService.DeleteAsync(supplierToDelete.Id);
|
||||||
suppliers = await SupplierService.GetAllAsync();
|
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)
|
private string? TruncateText(string? text, int maxLength)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
|
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ else
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Kerf Width</th>
|
<th>Kerf Width</th>
|
||||||
<th>Default</th>
|
<th>Default</th>
|
||||||
<th style="width: 140px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var tool in tools)
|
@foreach (var tool in pagedTools)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@tool.Name</td>
|
<td>@tool.Name</td>
|
||||||
@@ -90,13 +90,15 @@ else
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => Edit(tool)">Edit</button>
|
<button class="btn btn-sm btn-outline-primary" @onclick="() => Edit(tool)" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(tool)">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(tool)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager TotalCount="tools.Count" PageSize="pageSize" CurrentPage="currentPage" CurrentPageChanged="OnPageChanged" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,10 @@ else
|
|||||||
private bool showForm;
|
private bool showForm;
|
||||||
private bool saving;
|
private bool saving;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int pageSize = 25;
|
||||||
|
|
||||||
|
private IEnumerable<CuttingTool> pagedTools => tools.Skip((currentPage - 1) * pageSize).Take(pageSize);
|
||||||
|
|
||||||
private CuttingTool formTool = new();
|
private CuttingTool formTool = new();
|
||||||
private CuttingTool? editingTool;
|
private CuttingTool? editingTool;
|
||||||
@@ -207,9 +213,15 @@ else
|
|||||||
{
|
{
|
||||||
await JobService.DeleteCuttingToolAsync(toolToDelete.Id);
|
await JobService.DeleteCuttingToolAsync(toolToDelete.Id);
|
||||||
tools = await JobService.GetCuttingToolsAsync();
|
tools = await JobService.GetCuttingToolsAsync();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)tools.Count / pageSize);
|
||||||
|
if (currentPage > totalPages && totalPages > 0)
|
||||||
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPageChanged(int page) => currentPage = page;
|
||||||
|
|
||||||
private string FormatKerf(decimal kerf)
|
private string FormatKerf(decimal kerf)
|
||||||
{
|
{
|
||||||
// Show as fraction if it's a common value
|
// Show as fraction if it's a common value
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
@using CutList.Web.Data.Entities
|
||||||
|
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm" value="@Value.Shape" @onchange="OnShapeChanged">
|
||||||
|
<option value="">All Shapes</option>
|
||||||
|
@foreach (var shape in Enum.GetValues<MaterialShape>())
|
||||||
|
{
|
||||||
|
<option value="@shape">@shape.GetDisplayName()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm" value="@Value.Type" @onchange="OnTypeChanged">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
@foreach (var type in Enum.GetValues<MaterialType>())
|
||||||
|
{
|
||||||
|
<option value="@type">@type</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm" value="@(Value.Grade ?? "")" @onchange="OnGradeChanged">
|
||||||
|
<option value="">All Grades</option>
|
||||||
|
@foreach (var grade in AvailableGrades)
|
||||||
|
{
|
||||||
|
<option value="@grade">@grade</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" class="form-control form-control-sm" placeholder="Search..." value="@Value.SearchText" @oninput="OnSearchInput" />
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="OnClear" title="Clear filters">
|
||||||
|
<i class="bi bi-x-lg"></i> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public IEnumerable<string> AvailableGrades { get; set; } = Enumerable.Empty<string>();
|
||||||
|
[Parameter] public MaterialFilterState Value { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback<MaterialFilterState> ValueChanged { get; set; }
|
||||||
|
|
||||||
|
private async Task OnShapeChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
Value.Shape = Enum.TryParse<MaterialShape>(e.Value?.ToString(), out var shape) ? shape : null;
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTypeChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
Value.Type = Enum.TryParse<MaterialType>(e.Value?.ToString(), out var type) ? type : null;
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGradeChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var val = e.Value?.ToString();
|
||||||
|
Value.Grade = string.IsNullOrEmpty(val) ? null : val;
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSearchInput(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
Value.SearchText = e.Value?.ToString();
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClear()
|
||||||
|
{
|
||||||
|
Value.Shape = null;
|
||||||
|
Value.Type = null;
|
||||||
|
Value.Grade = null;
|
||||||
|
Value.SearchText = null;
|
||||||
|
await ValueChanged.InvokeAsync(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using CutList.Web.Data.Entities;
|
||||||
|
|
||||||
|
namespace CutList.Web.Components.Shared;
|
||||||
|
|
||||||
|
public class MaterialFilterState
|
||||||
|
{
|
||||||
|
public MaterialShape? Shape { get; set; }
|
||||||
|
public MaterialType? Type { get; set; }
|
||||||
|
public string? Grade { get; set; }
|
||||||
|
public string? SearchText { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@if (TotalCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
var totalPages = (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||||
|
var start = (CurrentPage - 1) * PageSize + 1;
|
||||||
|
var end = Math.Min(CurrentPage * PageSize, TotalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<small class="text-muted">Showing @start–@end of @TotalCount</small>
|
||||||
|
|
||||||
|
@if (totalPages > 1)
|
||||||
|
{
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item @(CurrentPage == 1 ? "disabled" : "")">
|
||||||
|
<button class="page-link" @onclick="() => SetPage(CurrentPage - 1)" disabled="@(CurrentPage == 1)">Previous</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
@foreach (var page in GetPageWindow(totalPages))
|
||||||
|
{
|
||||||
|
@if (page == -1)
|
||||||
|
{
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">…</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="page-item @(page == CurrentPage ? "active" : "")">
|
||||||
|
<button class="page-link" @onclick="() => SetPage(page)">@(page)</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<li class="page-item @(CurrentPage == totalPages ? "disabled" : "")">
|
||||||
|
<button class="page-link" @onclick="() => SetPage(CurrentPage + 1)" disabled="@(CurrentPage == totalPages)">Next</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int PageSize { get; set; } = 25;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int CurrentPage { get; set; } = 1;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> CurrentPageChanged { get; set; }
|
||||||
|
|
||||||
|
private async Task SetPage(int page)
|
||||||
|
{
|
||||||
|
if (page < 1 || page > (int)Math.Ceiling((double)TotalCount / PageSize))
|
||||||
|
return;
|
||||||
|
if (page == CurrentPage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentPage = page;
|
||||||
|
await CurrentPageChanged.InvokeAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<int> GetPageWindow(int totalPages)
|
||||||
|
{
|
||||||
|
const int maxVisible = 7;
|
||||||
|
|
||||||
|
if (totalPages <= maxVisible)
|
||||||
|
{
|
||||||
|
for (var i = 1; i <= totalPages; i++)
|
||||||
|
yield return i;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show first page
|
||||||
|
yield return 1;
|
||||||
|
|
||||||
|
var windowStart = Math.Max(2, CurrentPage - 2);
|
||||||
|
var windowEnd = Math.Min(totalPages - 1, CurrentPage + 2);
|
||||||
|
|
||||||
|
// Adjust window to show 5 middle pages when possible
|
||||||
|
if (windowEnd - windowStart < 4)
|
||||||
|
{
|
||||||
|
if (windowStart == 2)
|
||||||
|
windowEnd = Math.Min(totalPages - 1, windowStart + 4);
|
||||||
|
else
|
||||||
|
windowStart = Math.Max(2, windowEnd - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowStart > 2)
|
||||||
|
yield return -1; // ellipsis
|
||||||
|
|
||||||
|
for (var i = windowStart; i <= windowEnd; i++)
|
||||||
|
yield return i;
|
||||||
|
|
||||||
|
if (windowEnd < totalPages - 1)
|
||||||
|
yield return -1; // ellipsis
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
yield return totalPages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public class ApplicationDbContext : DbContext
|
|||||||
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)
|
||||||
{
|
{
|
||||||
@@ -281,6 +282,34 @@ 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 },
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ public class Job
|
|||||||
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 DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public DateTime? LockedAt { get; set; }
|
||||||
|
|
||||||
|
public bool IsLocked => LockedAt.HasValue;
|
||||||
|
|
||||||
public CuttingTool? CuttingTool { get; set; }
|
public CuttingTool? CuttingTool { get; set; }
|
||||||
public ICollection<JobPart> Parts { get; set; } = new List<JobPart>();
|
public ICollection<JobPart> Parts { get; set; } = new List<JobPart>();
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,896 @@
|
|||||||
|
// <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("20260207195807_AddPurchaseItem")]
|
||||||
|
partial class AddPurchaseItem
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
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<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DimensionType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("nvarchar(21)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MaterialDimensions");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("DimensionType").HasValue("MaterialDimensions");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
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?>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UnitPrice")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId");
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Thickness");
|
||||||
|
|
||||||
|
b.HasIndex("Leg1");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Angle");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("Web")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Channel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Thickness");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Width");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("FlatBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("WeightPerFoot")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("IBeam");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("NominalSize");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Pipe");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Width");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("RectangularTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.HasDiscriminator().HasValue("RoundBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("OuterDiameter");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("RoundTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Size");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("SquareBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Size");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("SquareTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.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 =>
|
||||||
|
{
|
||||||
|
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.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.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 =>
|
||||||
|
{
|
||||||
|
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("SupplierOfferings");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Offerings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPurchaseItem : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PurchaseItems",
|
||||||
|
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: true),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
JobId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
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.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PurchaseItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,899 @@
|
|||||||
|
// <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("20260207201007_AddJobLockedAt")]
|
||||||
|
partial class AddJobLockedAt
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
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<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");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DimensionType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("nvarchar(21)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MaterialDimensions");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("DimensionType").HasValue("MaterialDimensions");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
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?>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UnitPrice")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId");
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Thickness");
|
||||||
|
|
||||||
|
b.HasIndex("Leg1");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Angle");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("Web")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Channel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Thickness")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Thickness");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Width");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("FlatBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("WeightPerFoot")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.HasIndex("Height");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("IBeam");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("NominalSize");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Pipe");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Height")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Height");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.Property<decimal>("Width")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Width");
|
||||||
|
|
||||||
|
b.HasIndex("Width");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("RectangularTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.HasDiscriminator().HasValue("RoundBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("OuterDiameter");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("RoundTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Size");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("SquareBar");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Size");
|
||||||
|
|
||||||
|
b.Property<decimal>("Wall")
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)")
|
||||||
|
.HasColumnName("Wall");
|
||||||
|
|
||||||
|
b.HasIndex("Size");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("SquareTube");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.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 =>
|
||||||
|
{
|
||||||
|
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.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.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 =>
|
||||||
|
{
|
||||||
|
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("SupplierOfferings");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Offerings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobLockedAt : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "LockedAt",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LockedAt",
|
||||||
|
table: "Jobs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,9 @@ namespace CutList.Web.Migrations
|
|||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
.HasColumnType("nvarchar(20)");
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LockedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("nvarchar(100)");
|
.HasColumnType("nvarchar(100)");
|
||||||
@@ -289,6 +292,54 @@ namespace CutList.Web.Migrations
|
|||||||
b.UseTphMappingStrategy();
|
b.UseTphMappingStrategy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -727,6 +778,31 @@ 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")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,26 @@ public class JobService
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task LockAsync(int id)
|
||||||
|
{
|
||||||
|
var job = await _context.Jobs.FindAsync(id);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.LockedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnlockAsync(int id)
|
||||||
|
{
|
||||||
|
var job = await _context.Jobs.FindAsync(id);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.LockedAt = null;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(int id)
|
public async Task DeleteAsync(int id)
|
||||||
{
|
{
|
||||||
var job = await _context.Jobs.FindAsync(id);
|
var job = await _context.Jobs.FindAsync(id);
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using CutList.Web.Data;
|
||||||
|
using CutList.Web.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CutList.Web.Services;
|
||||||
|
|
||||||
|
public class PurchaseItemService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public PurchaseItemService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<PurchaseItem>> GetAllAsync(PurchaseItemStatus? status = null)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
item.CreatedAt = DateTime.UtcNow;
|
||||||
|
_context.PurchaseItems.Add(item);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateBulkAsync(List<PurchaseItem> items)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
|
_context.PurchaseItems.Update(item);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatusAsync(int id, PurchaseItemStatus status)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var item = await _context.PurchaseItems.FindAsync(id);
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
_context.PurchaseItems.Remove(item);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"
|
"DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,19 +258,73 @@
|
|||||||
font-size: 8pt;
|
font-size: 8pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
/* Compact summary: inline row instead of big cards */
|
||||||
|
.print-summary {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.4rem 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-summary > div {
|
||||||
|
flex: 1 !important;
|
||||||
|
max-width: none !important;
|
||||||
|
width: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-summary .card {
|
||||||
|
border: none !important;
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-summary .card-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-summary h2 {
|
||||||
|
font-size: 11pt !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-summary p {
|
||||||
|
font-size: 8pt !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide redundant stock summary (shown per-material) */
|
||||||
|
.print-stock-summary {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* General card print styles */
|
||||||
.card {
|
.card {
|
||||||
display: none !important;
|
border: 1px solid #ccc !important;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:not(.report-header h1) {
|
.card-header {
|
||||||
display: none !important;
|
background-color: #f0f0f0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted:not(.cut-list-report .text-muted) {
|
.badge {
|
||||||
display: none !important;
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce spacing */
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 0.25rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user