Compare commits
15 Commits
c23c92e852
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d3c92226c | |||
| c31769a746 | |||
| f04bf02c42 | |||
| dac2833dd1 | |||
| a226a1f652 | |||
| 5000021193 | |||
| 02e936febb | |||
| e13f876da6 | |||
| 1f3eb67eb7 | |||
| 2fdf006a8e | |||
| eee38a8473 | |||
| 59f86c8e79 | |||
| 891b214b29 | |||
| c5f366a3ef | |||
| 8926d44969 |
@@ -88,8 +88,8 @@ dotnet clean CutList.sln
|
||||
- **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`.
|
||||
### MaterialDimensions (TPC Inheritance)
|
||||
Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its own standalone table (`DimAngle`, `DimChannel`, `DimFlatBar`, `DimIBeam`, `DimPipe`, `DimRectangularTube`, `DimRoundBar`, `DimRoundTube`, `DimSquareBar`, `DimSquareTube`) with no base table. Each table has its own `Id` (shared sequence) and `MaterialId` FK. Each generates its own `SizeString` and `SortOrder`.
|
||||
|
||||
### StockItem
|
||||
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`
|
||||
@@ -115,6 +115,8 @@ Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensio
|
||||
### Job
|
||||
- `JobNumber` (auto-generated "JOB-#####", unique), `Name`, `Customer`, `CuttingToolId`, `Notes`
|
||||
- `LockedAt` (DateTime?) — set when materials ordered; `IsLocked` computed property
|
||||
- `OptimizationResultJson` (string?, nvarchar(max)) — serialized optimization results
|
||||
- `OptimizedAt` (DateTime?) — when optimization was last run
|
||||
- **Relationships**: `Parts` (1:many JobPart), `Stock` (1:many JobStock), `CuttingTool`
|
||||
|
||||
### JobPart
|
||||
@@ -146,14 +148,16 @@ Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensio
|
||||
### 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`
|
||||
- Parts: `AddPartAsync`, `UpdatePartAsync`, `DeletePartAsync` (all update job timestamp + clear optimization results)
|
||||
- Stock: `AddStockAsync`, `UpdateStockAsync`, `DeleteStockAsync` (all clear optimization results)
|
||||
- Optimization: `SaveOptimizationResultAsync`, `ClearOptimizationResultAsync`
|
||||
- Cutting tools: full CRUD with single-default enforcement
|
||||
|
||||
### CutListPackingService
|
||||
- `PackAsync(parts, kerfInches, jobStock?)` — runs optimization per material group
|
||||
- Separates results into `InStockBins` (from inventory) and `ToBePurchasedBins`
|
||||
- `GetSummary(result)` — calculates total bins, pieces, waste, efficiency %
|
||||
- `SerializeResult(result)` / `LoadSavedResult(json)` — JSON round-trip via DTO layer (`SavedOptimizationResult` etc.)
|
||||
|
||||
### PurchaseItemService
|
||||
- CRUD + `CreateBulkAsync` for batch creation from optimization results
|
||||
@@ -169,8 +173,7 @@ Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensio
|
||||
| `/` | 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 |
|
||||
| `/jobs/{Id}` | Jobs/Edit | Tabbed editor (Details, Parts, Stock, Results); locked jobs show banner + disable editing |
|
||||
| `/materials` | Materials/Index | Material list with MaterialFilter, pagination |
|
||||
| `/materials/new`, `/materials/{Id}` | Materials/Edit | Material + dimension form (varies by shape) |
|
||||
| `/stock` | Stock/Index | Stock items with MaterialFilter, quantity badges |
|
||||
@@ -200,6 +203,7 @@ Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensio
|
||||
- **Material selection flow** — Shape dropdown -> Size dropdown -> Length input -> Quantity (conditional dropdowns)
|
||||
- **Stock priority** — Lower number = used first; `-1` quantity = unlimited
|
||||
- **Job stock** — Jobs can use auto-discovered inventory OR define custom stock lengths
|
||||
- **Optimization persistence** — Results saved as JSON in `Job.OptimizationResultJson`; DTO layer (`SavedOptimizationResult` etc.) handles serialization since Core types use encapsulated collections; results auto-cleared when parts, stock, or cutting tool change
|
||||
- **Purchase flow** — Optimize job -> "Add to Order List" creates PurchaseItems + locks job -> Orders page manages status (Pending -> Ordered -> Received)
|
||||
- **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications
|
||||
- **Collections** — Encapsulated in Core; use `AsReadOnly()`, access via `Add*` methods
|
||||
@@ -216,6 +220,5 @@ Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensio
|
||||
| `CutList.Web/Data/ApplicationDbContext.cs` | EF Core context with all DbSets and configuration |
|
||||
| `CutList.Web/Services/JobService.cs` | Job orchestration (CRUD, parts, stock, tools, lock/unlock) |
|
||||
| `CutList.Web/Services/CutListPackingService.cs` | Bridges web entities to Core packing engine |
|
||||
| `CutList.Web/Components/Pages/Jobs/Edit.razor` | Job editor (tabbed: Details, Parts, Stock) |
|
||||
| `CutList.Web/Components/Pages/Jobs/Results.razor` | Optimization results + order creation |
|
||||
| `CutList.Web/Components/Pages/Jobs/Edit.razor` | Job editor (tabbed: Details, Parts, Stock, Results) |
|
||||
| `CutList/Presenters/MainFormPresenter.cs` | WinForms business logic orchestrator |
|
||||
|
||||
@@ -9,9 +9,9 @@ namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Default maximum number of items before falling back to AdvancedFitEngine.
|
||||
/// Testing showed 20 items is safe (~100ms worst case), while 21+ can take seconds.
|
||||
/// Testing showed 25 items is safe (~84ms worst case), while 30+ can take seconds.
|
||||
/// </summary>
|
||||
public const int DefaultMaxItems = 20;
|
||||
public const int DefaultMaxItems = 25;
|
||||
|
||||
private readonly IEngine _fallbackEngine;
|
||||
private readonly int _maxItems;
|
||||
@@ -67,7 +67,15 @@ namespace CutList.Core.Nesting
|
||||
BinCount = 0
|
||||
};
|
||||
|
||||
Search(sortedItems, 0, currentState, bestSolution, request);
|
||||
// Precompute suffix sums of item lengths (including spacing per item)
|
||||
// for lower-bound pruning. suffixVolume[i] = total volume of items[i..n-1].
|
||||
var suffixVolume = new double[sortedItems.Count + 1];
|
||||
for (int i = sortedItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
suffixVolume[i] = suffixVolume[i + 1] + sortedItems[i].Length + request.Spacing;
|
||||
}
|
||||
|
||||
Search(sortedItems, 0, currentState, bestSolution, request, suffixVolume);
|
||||
|
||||
// Build result from best solution
|
||||
var result = new PackResult();
|
||||
@@ -101,7 +109,8 @@ namespace CutList.Core.Nesting
|
||||
int itemIndex,
|
||||
SearchState current,
|
||||
SearchState best,
|
||||
PackingRequest request)
|
||||
PackingRequest request,
|
||||
double[] suffixVolume)
|
||||
{
|
||||
// All items placed - check if this is better
|
||||
if (itemIndex >= items.Count)
|
||||
@@ -123,6 +132,18 @@ namespace CutList.Core.Nesting
|
||||
if (current.BinCount >= request.MaxBinCount)
|
||||
return;
|
||||
|
||||
// Lower-bound pruning: remaining items need at least this many additional bins
|
||||
double remainingVolume = suffixVolume[itemIndex];
|
||||
double availableInExisting = 0;
|
||||
for (int b = 0; b < current.Bins.Count; b++)
|
||||
{
|
||||
availableInExisting += request.StockLength - GetBinUsedLength(current.Bins[b], request.Spacing);
|
||||
}
|
||||
double overflow = remainingVolume - availableInExisting;
|
||||
int additionalBinsNeeded = overflow > 0 ? (int)Math.Ceiling(overflow / request.StockLength) : 0;
|
||||
if (current.BinCount + additionalBinsNeeded >= best.BinCount)
|
||||
return;
|
||||
|
||||
var item = items[itemIndex];
|
||||
|
||||
// Symmetry breaking: if this item has the same length as the previous item,
|
||||
@@ -148,7 +169,7 @@ namespace CutList.Core.Nesting
|
||||
current.Bins[i].Add(item);
|
||||
var prevBinIndex = current.LastBinIndexUsed;
|
||||
current.LastBinIndexUsed = i;
|
||||
Search(items, itemIndex + 1, current, best, request);
|
||||
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||
current.LastBinIndexUsed = prevBinIndex;
|
||||
current.Bins[i].RemoveAt(current.Bins[i].Count - 1);
|
||||
}
|
||||
@@ -162,7 +183,7 @@ namespace CutList.Core.Nesting
|
||||
current.BinCount++;
|
||||
var prevBinIndex = current.LastBinIndexUsed;
|
||||
current.LastBinIndexUsed = newBinIndex;
|
||||
Search(items, itemIndex + 1, current, best, request);
|
||||
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||
current.LastBinIndexUsed = prevBinIndex;
|
||||
current.Bins.RemoveAt(current.Bins.Count - 1);
|
||||
current.BinCount--;
|
||||
|
||||
+231
-1
@@ -109,6 +109,109 @@ public class ApiClient
|
||||
|
||||
#endregion
|
||||
|
||||
#region Jobs
|
||||
|
||||
public async Task<List<ApiJobDto>> GetJobsAsync()
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiJobDto>>("api/jobs") ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> GetJobAsync(int id)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<ApiJobDetailDto>($"api/jobs/{id}");
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> CreateJobAsync(string? name, string? customer, int? cuttingToolId, string? notes)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("api/jobs", new { Name = name, Customer = customer, CuttingToolId = cuttingToolId, Notes = notes });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobDetailDto>();
|
||||
}
|
||||
|
||||
public async Task<ApiJobDetailDto?> UpdateJobAsync(int id, string? name, string? customer, int? cuttingToolId, string? notes)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"api/jobs/{id}", new { Name = name, Customer = customer, CuttingToolId = cuttingToolId, Notes = notes });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobDetailDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobAsync(int id)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiJobPartDto?> AddJobPartAsync(int jobId, int materialId, string name, string length, int quantity)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/parts", new { MaterialId = materialId, Name = name, Length = length, Quantity = quantity });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobPartDto>();
|
||||
}
|
||||
|
||||
public async Task<ApiJobPartDto?> UpdateJobPartAsync(int jobId, int partId, int? materialId, string? name, string? length, int? quantity)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"api/jobs/{jobId}/parts/{partId}", new { MaterialId = materialId, Name = name, Length = length, Quantity = quantity });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobPartDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobPartAsync(int jobId, int partId)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{jobId}/parts/{partId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiJobStockDto?> AddJobStockAsync(int jobId, int materialId, int? stockItemId, string length, int quantity, bool isCustomLength, int priority)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/stock", new
|
||||
{
|
||||
MaterialId = materialId,
|
||||
StockItemId = stockItemId,
|
||||
Length = length,
|
||||
Quantity = quantity,
|
||||
IsCustomLength = isCustomLength,
|
||||
Priority = priority
|
||||
});
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiJobStockDto>();
|
||||
}
|
||||
|
||||
public async Task DeleteJobStockAsync(int jobId, int stockId)
|
||||
{
|
||||
var response = await _http.DeleteAsync($"api/jobs/{jobId}/stock/{stockId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<ApiPackResponseDto?> PackJobAsync(int jobId, decimal? kerfOverride = null)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/jobs/{jobId}/pack", new { KerfOverride = kerfOverride });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiPackResponseDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cutting Tools
|
||||
|
||||
public async Task<List<ApiCuttingToolDto>> GetCuttingToolsAsync(bool includeInactive = false)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiCuttingToolDto>>($"api/cutting-tools?includeInactive={includeInactive}") ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiCuttingToolDto?> GetDefaultCuttingToolAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetFromJsonAsync<ApiCuttingToolDto>("api/cutting-tools/default");
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Offerings
|
||||
|
||||
public async Task<List<ApiOfferingDto>> GetOfferingsForSupplierAsync(int supplierId)
|
||||
@@ -153,7 +256,134 @@ public class ApiConflictException : Exception
|
||||
public ApiConflictException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
#region API Response DTOs
|
||||
#region API Response DTOs — Jobs & Cutting Tools
|
||||
|
||||
public class ApiJobDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
}
|
||||
|
||||
public class ApiJobDetailDto : ApiJobDto
|
||||
{
|
||||
public List<ApiJobPartDto> Parts { get; set; } = new();
|
||||
public List<ApiJobStockDto> Stock { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiJobPartDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class ApiJobStockDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int? StockItemId { get; set; }
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class ApiCuttingToolDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class ApiPackResponseDto
|
||||
{
|
||||
public List<ApiMaterialPackResultDto> Materials { get; set; } = new();
|
||||
public ApiPackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiMaterialPackResultDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public List<ApiPackedBinDto> InStockBins { get; set; } = new();
|
||||
public List<ApiPackedBinDto> ToBePurchasedBins { get; set; } = new();
|
||||
public List<ApiPackedItemDto> ItemsNotPlaced { get; set; } = new();
|
||||
public ApiMaterialPackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiPackedBinDto
|
||||
{
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public double UsedInches { get; set; }
|
||||
public string UsedFormatted { get; set; } = string.Empty;
|
||||
public double WasteInches { get; set; }
|
||||
public string WasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public List<ApiPackedItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiPackedItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ApiPackingSummaryDto
|
||||
{
|
||||
public int TotalInStockBins { get; set; }
|
||||
public int TotalToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public string TotalMaterialFormatted { get; set; } = string.Empty;
|
||||
public double TotalUsedInches { get; set; }
|
||||
public string TotalUsedFormatted { get; set; } = string.Empty;
|
||||
public double TotalWasteInches { get; set; }
|
||||
public string TotalWasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public int TotalItemsNotPlaced { get; set; }
|
||||
public List<ApiMaterialPackingSummaryDto> MaterialSummaries { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiMaterialPackingSummaryDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int InStockBins { get; set; }
|
||||
public int ToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public double TotalUsedInches { get; set; }
|
||||
public double TotalWasteInches { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API Response DTOs — Inventory
|
||||
|
||||
public class ApiSupplierDto
|
||||
{
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
using System.ComponentModel;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace CutList.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// MCP tools for job management - creating jobs, managing parts/stock, and running optimization.
|
||||
/// All calls go through the CutList.Web REST API via ApiClient.
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class JobTools
|
||||
{
|
||||
private readonly ApiClient _api;
|
||||
|
||||
public JobTools(ApiClient api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
[McpServerTool(Name = "list_jobs"), Description("Lists all jobs in the system with summary info (job number, name, customer, part/stock counts).")]
|
||||
public async Task<JobListResult> ListJobs()
|
||||
{
|
||||
var jobs = await _api.GetJobsAsync();
|
||||
|
||||
return new JobListResult
|
||||
{
|
||||
Success = true,
|
||||
Jobs = jobs.Select(j => new JobSummaryDto
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingToolName,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.PartCount,
|
||||
StockCount = j.StockCount
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "get_job"), Description("Gets full job details including all parts and stock assignments.")]
|
||||
public async Task<JobDetailResult> GetJob(
|
||||
[Description("Job ID")]
|
||||
int jobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.GetJobAsync(jobId);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "create_job"), Description("Creates a new job. Returns the created job with its auto-generated job number.")]
|
||||
public async Task<JobDetailResult> CreateJob(
|
||||
[Description("Job name/description")]
|
||||
string? name = null,
|
||||
[Description("Customer name")]
|
||||
string? customer = null,
|
||||
[Description("Cutting tool ID (use list_cutting_tools to find IDs). If not set, uses the default tool.")]
|
||||
int? cuttingToolId = null,
|
||||
[Description("Notes about the job")]
|
||||
string? notes = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.CreateJobAsync(name, customer, cuttingToolId, notes);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = "Failed to create job" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "update_job"), Description("Updates job details (name, customer, cutting tool, notes). Only provided fields are updated.")]
|
||||
public async Task<JobDetailResult> UpdateJob(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("New job name")]
|
||||
string? name = null,
|
||||
[Description("New customer name")]
|
||||
string? customer = null,
|
||||
[Description("New cutting tool ID")]
|
||||
int? cuttingToolId = null,
|
||||
[Description("New notes")]
|
||||
string? notes = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await _api.UpdateJobAsync(jobId, name, customer, cuttingToolId, notes);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found" };
|
||||
|
||||
return new JobDetailResult
|
||||
{
|
||||
Success = true,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job"), Description("Deletes a job and all its parts and stock assignments.")]
|
||||
public async Task<SimpleResult> DeleteJob(
|
||||
[Description("Job ID")]
|
||||
int jobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobAsync(jobId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parts
|
||||
|
||||
[McpServerTool(Name = "add_job_part"), Description("Adds a single part to a job.")]
|
||||
public async Task<JobPartResult> AddJobPart(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Material ID (use list_materials to find IDs)")]
|
||||
int materialId,
|
||||
[Description("Part name/label (e.g., 'Top Rail', 'Picket')")]
|
||||
string name,
|
||||
[Description("Part length (e.g., '36\"', '4\\' 6\"', '54.5')")]
|
||||
string length,
|
||||
[Description("Quantity needed (default 1)")]
|
||||
int quantity = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var part = await _api.AddJobPartAsync(jobId, materialId, name, length, quantity);
|
||||
if (part == null)
|
||||
return new JobPartResult { Success = false, Error = "Failed to add part" };
|
||||
|
||||
return new JobPartResult
|
||||
{
|
||||
Success = true,
|
||||
Part = MapPart(part)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobPartResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "add_job_parts"), Description("Batch adds multiple parts to a job. Ideal for entering a full BOM (bill of materials). Returns the complete job state after all parts are added.")]
|
||||
public async Task<JobDetailResult> AddJobParts(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Array of parts to add. Each needs: materialId (int), name (string), length (string like \"36\\\"\" or \"4' 6\\\"\"), quantity (int)")]
|
||||
PartEntry[] parts)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
int added = 0;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.AddJobPartAsync(jobId, part.MaterialId, part.Name, part.Length, part.Quantity);
|
||||
added++;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
errors.Add($"Failed to add '{part.Name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the full job state
|
||||
try
|
||||
{
|
||||
var job = await _api.GetJobAsync(jobId);
|
||||
if (job == null)
|
||||
return new JobDetailResult { Success = false, Error = $"Job {jobId} not found after adding parts" };
|
||||
|
||||
var result = new JobDetailResult
|
||||
{
|
||||
Success = errors.Count == 0,
|
||||
Job = MapJobDetail(job)
|
||||
};
|
||||
|
||||
if (errors.Count > 0)
|
||||
result.Error = $"Added {added}/{parts.Length} parts. Errors: {string.Join("; ", errors)}";
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobDetailResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job_part"), Description("Removes a part from a job.")]
|
||||
public async Task<SimpleResult> DeleteJobPart(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Part ID (from get_job results)")]
|
||||
int partId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobPartAsync(jobId, partId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock
|
||||
|
||||
[McpServerTool(Name = "add_job_stock"), Description("Adds a stock material assignment to a job. Stock defines what material lengths are available for cutting.")]
|
||||
public async Task<JobStockResult> AddJobStock(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Material ID (must match the material used by parts)")]
|
||||
int materialId,
|
||||
[Description("Stock length (e.g., '20'', '240', '20 ft')")]
|
||||
string length,
|
||||
[Description("Quantity available (-1 for unlimited, default -1)")]
|
||||
int quantity = -1,
|
||||
[Description("Stock item ID from inventory (optional - links to tracked inventory)")]
|
||||
int? stockItemId = null,
|
||||
[Description("True if this is a custom length not from inventory (default false)")]
|
||||
bool isCustomLength = false,
|
||||
[Description("Priority - lower number = used first (default 10)")]
|
||||
int priority = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stock = await _api.AddJobStockAsync(jobId, materialId, stockItemId, length, quantity, isCustomLength, priority);
|
||||
if (stock == null)
|
||||
return new JobStockResult { Success = false, Error = "Failed to add stock" };
|
||||
|
||||
return new JobStockResult
|
||||
{
|
||||
Success = true,
|
||||
Stock = MapStock(stock)
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new JobStockResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "delete_job_stock"), Description("Removes a stock assignment from a job.")]
|
||||
public async Task<SimpleResult> DeleteJobStock(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Stock ID (from get_job results)")]
|
||||
int stockId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _api.DeleteJobStockAsync(jobId, stockId);
|
||||
return new SimpleResult { Success = true };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new SimpleResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimization
|
||||
|
||||
[McpServerTool(Name = "optimize_job"), Description("Runs bin packing optimization on a job. The job must have parts defined. If stock is defined, it will be used; otherwise the optimizer uses available inventory. Returns optimized cut layouts per material with efficiency stats.")]
|
||||
public async Task<OptimizeJobResult> OptimizeJob(
|
||||
[Description("Job ID")]
|
||||
int jobId,
|
||||
[Description("Optional kerf override in inches (e.g., 0.125). If not set, uses the job's cutting tool kerf.")]
|
||||
double? kerfOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _api.PackJobAsync(jobId, kerfOverride.HasValue ? (decimal)kerfOverride.Value : null);
|
||||
if (result == null)
|
||||
return new OptimizeJobResult { Success = false, Error = "Optimization returned no results" };
|
||||
|
||||
return new OptimizeJobResult
|
||||
{
|
||||
Success = true,
|
||||
Materials = result.Materials.Select(m => new OptMaterialResultDto
|
||||
{
|
||||
MaterialId = m.MaterialId,
|
||||
MaterialName = m.MaterialName,
|
||||
InStockBins = m.InStockBins.Select(MapBin).ToList(),
|
||||
ToBePurchasedBins = m.ToBePurchasedBins.Select(MapBin).ToList(),
|
||||
ItemsNotPlaced = m.ItemsNotPlaced.Select(i => new OptItemDto { Name = i.Name, LengthInches = i.LengthInches, LengthFormatted = i.LengthFormatted }).ToList(),
|
||||
Summary = MapMaterialSummary(m.Summary)
|
||||
}).ToList(),
|
||||
Summary = new OptSummaryDto
|
||||
{
|
||||
TotalInStockBins = result.Summary.TotalInStockBins,
|
||||
TotalToBePurchasedBins = result.Summary.TotalToBePurchasedBins,
|
||||
TotalPieces = result.Summary.TotalPieces,
|
||||
TotalMaterialFormatted = result.Summary.TotalMaterialFormatted,
|
||||
TotalUsedFormatted = result.Summary.TotalUsedFormatted,
|
||||
TotalWasteFormatted = result.Summary.TotalWasteFormatted,
|
||||
Efficiency = result.Summary.Efficiency,
|
||||
TotalItemsNotPlaced = result.Summary.TotalItemsNotPlaced
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new OptimizeJobResult { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cutting Tools
|
||||
|
||||
[McpServerTool(Name = "list_cutting_tools"), Description("Lists all available cutting tools with their kerf (blade width) values.")]
|
||||
public async Task<CuttingToolListResult> ListCuttingTools(
|
||||
[Description("Include inactive tools (default false)")]
|
||||
bool includeInactive = false)
|
||||
{
|
||||
var tools = await _api.GetCuttingToolsAsync(includeInactive);
|
||||
|
||||
return new CuttingToolListResult
|
||||
{
|
||||
Success = true,
|
||||
Tools = tools.Select(t => new CuttingToolSummaryDto
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
KerfInches = t.KerfInches,
|
||||
IsDefault = t.IsDefault,
|
||||
IsActive = t.IsActive
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mapping Helpers
|
||||
|
||||
private static JobDetailDto MapJobDetail(ApiJobDetailDto j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingToolName,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.PartCount,
|
||||
StockCount = j.StockCount,
|
||||
Parts = j.Parts.Select(MapPart).ToList(),
|
||||
Stock = j.Stock.Select(MapStock).ToList()
|
||||
};
|
||||
|
||||
private static JobPartSummaryDto MapPart(ApiJobPartDto p) => new()
|
||||
{
|
||||
Id = p.Id,
|
||||
MaterialId = p.MaterialId,
|
||||
MaterialName = p.MaterialName,
|
||||
Name = p.Name,
|
||||
LengthInches = p.LengthInches,
|
||||
LengthFormatted = p.LengthFormatted,
|
||||
Quantity = p.Quantity
|
||||
};
|
||||
|
||||
private static JobStockSummaryDto MapStock(ApiJobStockDto s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.MaterialName,
|
||||
StockItemId = s.StockItemId,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = s.LengthFormatted,
|
||||
Quantity = s.Quantity,
|
||||
IsCustomLength = s.IsCustomLength,
|
||||
Priority = s.Priority
|
||||
};
|
||||
|
||||
private static OptBinDto MapBin(ApiPackedBinDto b) => new()
|
||||
{
|
||||
LengthFormatted = b.LengthFormatted,
|
||||
UsedFormatted = b.UsedFormatted,
|
||||
WasteFormatted = b.WasteFormatted,
|
||||
Efficiency = Math.Round(b.Efficiency, 1),
|
||||
Items = b.Items.Select(i => new OptItemDto
|
||||
{
|
||||
Name = i.Name,
|
||||
LengthInches = i.LengthInches,
|
||||
LengthFormatted = i.LengthFormatted
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
private static OptMaterialSummaryDto MapMaterialSummary(ApiMaterialPackingSummaryDto s) => new()
|
||||
{
|
||||
InStockBins = s.InStockBins,
|
||||
ToBePurchasedBins = s.ToBePurchasedBins,
|
||||
TotalPieces = s.TotalPieces,
|
||||
Efficiency = Math.Round(s.Efficiency, 1),
|
||||
ItemsNotPlaced = s.ItemsNotPlaced
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Job Tool DTOs
|
||||
|
||||
public class PartEntry
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class SimpleResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
public class JobSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
}
|
||||
|
||||
public class JobDetailDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
public List<JobPartSummaryDto> Parts { get; set; } = new();
|
||||
public List<JobStockSummaryDto> Stock { get; set; } = new();
|
||||
}
|
||||
|
||||
public class JobPartSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
public class JobStockSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int? StockItemId { get; set; }
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
public class JobListResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<JobSummaryDto> Jobs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class JobDetailResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobDetailDto? Job { get; set; }
|
||||
}
|
||||
|
||||
public class JobPartResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobPartSummaryDto? Part { get; set; }
|
||||
}
|
||||
|
||||
public class JobStockResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public JobStockSummaryDto? Stock { get; set; }
|
||||
}
|
||||
|
||||
public class CuttingToolSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CuttingToolListResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<CuttingToolSummaryDto> Tools { get; set; } = new();
|
||||
}
|
||||
|
||||
// Optimization result DTOs — streamlined for LLM consumption
|
||||
public class OptimizeJobResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<OptMaterialResultDto> Materials { get; set; } = new();
|
||||
public OptSummaryDto? Summary { get; set; }
|
||||
}
|
||||
|
||||
public class OptMaterialResultDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public List<OptBinDto> InStockBins { get; set; } = new();
|
||||
public List<OptBinDto> ToBePurchasedBins { get; set; } = new();
|
||||
public List<OptItemDto> ItemsNotPlaced { get; set; } = new();
|
||||
public OptMaterialSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OptBinDto
|
||||
{
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public string UsedFormatted { get; set; } = string.Empty;
|
||||
public string WasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public List<OptItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OptItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class OptSummaryDto
|
||||
{
|
||||
public int TotalInStockBins { get; set; }
|
||||
public int TotalToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public string TotalMaterialFormatted { get; set; } = string.Empty;
|
||||
public string TotalUsedFormatted { get; set; } = string.Empty;
|
||||
public string TotalWasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public int TotalItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
public class OptMaterialSummaryDto
|
||||
{
|
||||
public int InStockBins { get; set; }
|
||||
public int ToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -3,7 +3,12 @@
|
||||
@inject JobService JobService
|
||||
@inject MaterialService MaterialService
|
||||
@inject StockItemService StockItemService
|
||||
@inject CutListPackingService PackingService
|
||||
@inject PurchaseItemService PurchaseItemService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
@using CutList.Core
|
||||
@using CutList.Core.Nesting
|
||||
@using CutList.Core.Formatting
|
||||
@using CutList.Web.Data.Entities
|
||||
|
||||
@@ -11,10 +16,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>@(IsNew ? "New Job" : job.DisplayName)</h1>
|
||||
@if (!IsNew)
|
||||
{
|
||||
<a href="jobs/@Id/results" class="btn btn-success">Run Optimization</a>
|
||||
}
|
||||
<a href="jobs" class="btn btn-outline-secondary">Back to Jobs</a>
|
||||
</div>
|
||||
|
||||
@if (!IsNew && job.IsLocked)
|
||||
@@ -73,6 +75,16 @@ else
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(activeTab == Tab.Results ? "active" : "")"
|
||||
@onclick="() => SetTab(Tab.Results)" type="button">
|
||||
Results
|
||||
@if (summary != null)
|
||||
{
|
||||
<span class="badge bg-success ms-1">@summary.Efficiency.ToString("F0")%</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -92,6 +104,10 @@ else
|
||||
{
|
||||
@RenderStockTab()
|
||||
}
|
||||
else if (activeTab == Tab.Results)
|
||||
{
|
||||
@RenderResultsTab()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -102,14 +118,15 @@ else
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(editingPart == null ? "Add Part" : "Edit Part")</h5>
|
||||
<h5 class="modal-title">@(editingPart == null ? "Add Parts" : "Edit Part")</h5>
|
||||
<button type="button" class="btn-close" @onclick="CancelPartForm"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Shape</label>
|
||||
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged">
|
||||
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged"
|
||||
disabled="@(editingPart != null)">
|
||||
<option value="">-- Select --</option>
|
||||
@foreach (var shape in DistinctShapes)
|
||||
{
|
||||
@@ -119,7 +136,7 @@ else
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Size</label>
|
||||
<select class="form-select" @bind="newPart.MaterialId" disabled="@(!selectedShape.HasValue)">
|
||||
<select class="form-select" @bind="partSelectedMaterialId" disabled="@(!selectedShape.HasValue || editingPart != null)">
|
||||
<option value="0">-- Select --</option>
|
||||
@foreach (var material in FilteredMaterials)
|
||||
{
|
||||
@@ -127,19 +144,63 @@ else
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Length</label>
|
||||
<LengthInput @bind-Value="newPart.LengthInches" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<input type="text" class="form-control" @bind="newPart.Name" placeholder="Part name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (editingPart != null)
|
||||
{
|
||||
@* Edit mode: single row *@
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Length</label>
|
||||
<LengthInput @bind-Value="newPart.LengthInches" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Name <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<input type="text" class="form-control" @bind="newPart.Name" placeholder="Part name" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Add mode: multi-row table *@
|
||||
<table class="table table-sm align-middle mb-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Length</th>
|
||||
<th style="width: 100px;">Qty</th>
|
||||
<th>Name <span class="text-muted fw-normal">(optional)</span></th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (var i = 0; i < partRows.Count; i++)
|
||||
{
|
||||
var row = partRows[i];
|
||||
<tr>
|
||||
<td><LengthInput @bind-Value="row.LengthInches" /></td>
|
||||
<td><input type="number" class="form-control form-control-sm" @bind="row.Quantity" @bind:event="oninput" min="1" /></td>
|
||||
<td><input type="text" class="form-control form-control-sm" @bind="row.Name" @bind:event="oninput" placeholder="Part name" /></td>
|
||||
<td>
|
||||
@if (partRows.Count > 1)
|
||||
{
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" @onclick="() => RemovePartRow(row)" title="Remove">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="AddPartRow">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Row
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(partErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mt-3 mb-0">@partErrorMessage</div>
|
||||
@@ -148,7 +209,14 @@ else
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="SavePartAsync">
|
||||
@(editingPart == null ? "Add Part" : "Save Changes")
|
||||
@if (editingPart != null)
|
||||
{
|
||||
<text>Save Changes</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Add @partRows.Count Part@(partRows.Count != 1 ? "s" : "")</text>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,7 +321,7 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
private enum Tab { Details, Parts, Stock }
|
||||
private enum Tab { Details, Parts, Stock, Results }
|
||||
|
||||
[Parameter]
|
||||
public int? Id { get; set; }
|
||||
@@ -275,6 +343,8 @@ else
|
||||
private JobPart? editingPart;
|
||||
private string? partErrorMessage;
|
||||
private MaterialShape? selectedShape;
|
||||
private int partSelectedMaterialId;
|
||||
private List<PartRow> partRows = new();
|
||||
|
||||
// Stock form
|
||||
private bool showStockForm;
|
||||
@@ -292,12 +362,20 @@ else
|
||||
private List<ImportStockCandidate> importCandidates = new();
|
||||
private string? importErrorMessage;
|
||||
|
||||
// Results tab
|
||||
private MultiMaterialPackResult? packResult;
|
||||
private MultiMaterialPackingSummary? summary;
|
||||
private bool optimizing;
|
||||
private bool addingToOrderList;
|
||||
private bool addedToOrderList;
|
||||
|
||||
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
||||
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
|
||||
? Enumerable.Empty<Material>()
|
||||
: materials.Where(m => m.Shape == selectedShape.Value).OrderBy(m => m.SortOrder).ThenBy(m => m.Size);
|
||||
|
||||
private bool IsNew => !Id.HasValue;
|
||||
private bool CanOptimize => job.Parts.Count > 0 && job.CuttingToolId != null;
|
||||
|
||||
private async Task UnlockJob()
|
||||
{
|
||||
@@ -322,6 +400,12 @@ else
|
||||
return;
|
||||
}
|
||||
job = existing;
|
||||
|
||||
// Load saved optimization results if available
|
||||
if (job.OptimizationResultJson != null)
|
||||
{
|
||||
LoadSavedResults();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -336,6 +420,25 @@ else
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private void LoadSavedResults()
|
||||
{
|
||||
try
|
||||
{
|
||||
packResult = PackingService.LoadSavedResult(job.OptimizationResultJson!);
|
||||
if (packResult != null)
|
||||
{
|
||||
summary = PackingService.GetSummary(packResult);
|
||||
addedToOrderList = job.IsLocked;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Invalid JSON — treat as no results
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
}
|
||||
|
||||
private RenderFragment RenderDetailsForm() => __builder =>
|
||||
{
|
||||
<div class="card">
|
||||
@@ -474,6 +577,10 @@ else
|
||||
else
|
||||
{
|
||||
await JobService.UpdateAsync(job);
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
// Clear in-memory results since they were invalidated
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -488,15 +595,28 @@ else
|
||||
editingPart = null;
|
||||
newPart = new JobPart { JobId = Id!.Value, Quantity = 1 };
|
||||
selectedShape = null;
|
||||
partSelectedMaterialId = 0;
|
||||
partRows = new List<PartRow> { new PartRow() };
|
||||
showPartForm = true;
|
||||
partErrorMessage = null;
|
||||
}
|
||||
|
||||
private void OnShapeChanged()
|
||||
{
|
||||
partSelectedMaterialId = 0;
|
||||
newPart.MaterialId = 0;
|
||||
}
|
||||
|
||||
private void AddPartRow()
|
||||
{
|
||||
partRows.Add(new PartRow());
|
||||
}
|
||||
|
||||
private void RemovePartRow(PartRow row)
|
||||
{
|
||||
partRows.Remove(row);
|
||||
}
|
||||
|
||||
private void EditPart(JobPart part)
|
||||
{
|
||||
editingPart = part;
|
||||
@@ -511,6 +631,8 @@ else
|
||||
SortOrder = part.SortOrder
|
||||
};
|
||||
selectedShape = part.Material?.Shape;
|
||||
partSelectedMaterialId = part.MaterialId;
|
||||
partRows.Clear();
|
||||
showPartForm = true;
|
||||
partErrorMessage = null;
|
||||
}
|
||||
@@ -531,42 +653,75 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPart.MaterialId == 0)
|
||||
if (partSelectedMaterialId == 0)
|
||||
{
|
||||
partErrorMessage = "Please select a size";
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPart.LengthInches <= 0)
|
||||
if (editingPart != null)
|
||||
{
|
||||
partErrorMessage = "Length must be greater than zero";
|
||||
return;
|
||||
}
|
||||
// Edit mode: single part
|
||||
if (newPart.LengthInches <= 0)
|
||||
{
|
||||
partErrorMessage = "Length must be greater than zero";
|
||||
return;
|
||||
}
|
||||
if (newPart.Quantity < 1)
|
||||
{
|
||||
partErrorMessage = "Quantity must be at least 1";
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPart.Quantity < 1)
|
||||
{
|
||||
partErrorMessage = "Quantity must be at least 1";
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingPart == null)
|
||||
{
|
||||
await JobService.AddPartAsync(newPart);
|
||||
newPart.MaterialId = partSelectedMaterialId;
|
||||
await JobService.UpdatePartAsync(newPart);
|
||||
}
|
||||
else
|
||||
{
|
||||
await JobService.UpdatePartAsync(newPart);
|
||||
// Add mode: multiple rows
|
||||
for (int i = 0; i < partRows.Count; i++)
|
||||
{
|
||||
var row = partRows[i];
|
||||
if (row.LengthInches <= 0)
|
||||
{
|
||||
partErrorMessage = $"Row {i + 1}: Length must be greater than zero";
|
||||
return;
|
||||
}
|
||||
if (row.Quantity < 1)
|
||||
{
|
||||
partErrorMessage = $"Row {i + 1}: Quantity must be at least 1";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var row in partRows)
|
||||
{
|
||||
var part = new JobPart
|
||||
{
|
||||
JobId = Id!.Value,
|
||||
MaterialId = partSelectedMaterialId,
|
||||
LengthInches = row.LengthInches,
|
||||
Quantity = row.Quantity,
|
||||
Name = row.Name ?? string.Empty
|
||||
};
|
||||
await JobService.AddPartAsync(part);
|
||||
}
|
||||
}
|
||||
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
showPartForm = false;
|
||||
editingPart = null;
|
||||
// Results were cleared by the service
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
|
||||
private async Task DeletePart(JobPart part)
|
||||
{
|
||||
await JobService.DeletePartAsync(part.Id);
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
|
||||
// Stock tab
|
||||
@@ -582,10 +737,7 @@ else
|
||||
title="@(job.Parts.Count == 0 ? "Add parts first to match against inventory" : "Find and import stock matching your parts")">
|
||||
Import from Inventory
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" @onclick="ShowAddStockFromInventory">Add from Inventory</button>
|
||||
<button class="btn btn-outline-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -777,16 +929,351 @@ else
|
||||
</div>
|
||||
};
|
||||
|
||||
private void ShowAddStockFromInventory()
|
||||
// Results tab
|
||||
private RenderFragment RenderResultsTab() => __builder =>
|
||||
{
|
||||
editingStock = null;
|
||||
newStock = new JobStock { JobId = Id!.Value, Quantity = 1, Priority = 10 };
|
||||
stockSelectedShape = null;
|
||||
stockSelectedMaterialId = 0;
|
||||
availableStockItems.Clear();
|
||||
showStockForm = true;
|
||||
showCustomStockForm = false;
|
||||
stockErrorMessage = null;
|
||||
@if (!CanOptimize)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<h5 class="mb-2">Cannot Optimize</h5>
|
||||
<ul class="mb-0">
|
||||
@if (job.Parts.Count == 0)
|
||||
{
|
||||
<li>No parts defined. Switch to the <button class="btn btn-link p-0" @onclick="() => SetTab(Tab.Parts)">Parts tab</button> to add parts.</li>
|
||||
}
|
||||
@if (job.CuttingToolId == null)
|
||||
{
|
||||
<li>No cutting tool selected. Switch to the <button class="btn btn-link p-0" @onclick="() => SetTab(Tab.Details)">Details tab</button> to select a cutting tool.</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-success" @onclick="RunOptimization" disabled="@(optimizing || job.IsLocked)">
|
||||
@if (optimizing)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
<text>Optimizing...</text>
|
||||
}
|
||||
else if (packResult != null)
|
||||
{
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||
<text>Re-Optimize</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-scissors me-1"></i>
|
||||
<text>Optimize</text>
|
||||
}
|
||||
</button>
|
||||
@if (packResult != null)
|
||||
{
|
||||
<button class="btn btn-outline-secondary ms-2" @onclick="PrintReport">
|
||||
<i class="bi bi-printer me-1"></i> Print Report
|
||||
</button>
|
||||
}
|
||||
@if (job.OptimizedAt.HasValue)
|
||||
{
|
||||
<span class="text-muted ms-3">
|
||||
Last optimized: @job.OptimizedAt.Value.ToLocalTime().ToString("g")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (packResult != null && summary != null)
|
||||
{
|
||||
@if (summary.TotalItemsNotPlaced > 0)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<h5>Items Not Placed</h5>
|
||||
<p class="mb-0">Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Overall Summary Cards -->
|
||||
<div class="row mb-4 print-summary">
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
|
||||
<p class="card-text text-muted">Total Stock Bars</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@summary.TotalPieces</h2>
|
||||
<p class="card-text text-muted">Total Pieces</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
|
||||
<p class="card-text text-muted">Total Waste</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@summary.Efficiency.ToString("F1")%</h2>
|
||||
<p class="card-text text-muted">Efficiency</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purchase List -->
|
||||
<div class="card mb-4 print-purchase-list">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-cart me-2"></i>Purchase List</h5>
|
||||
@if (summary.TotalToBePurchasedBins > 0)
|
||||
{
|
||||
@if (addedToOrderList)
|
||||
{
|
||||
<span class="badge bg-success"><i class="bi bi-check-lg me-1"></i>Added to orders</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-warning btn-sm" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
||||
@if (addingToOrderList)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
}
|
||||
<i class="bi bi-cart-plus me-1"></i>Add to Order List
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (summary.TotalToBePurchasedBins == 0)
|
||||
{
|
||||
<p class="text-muted mb-0">Everything is available in stock. No purchases needed.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (addedToOrderList)
|
||||
{
|
||||
<div class="alert alert-success py-2 mb-3">
|
||||
Items added to order list. <a href="orders">View Orders</a>
|
||||
</div>
|
||||
}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Material</th>
|
||||
<th>Length</th>
|
||||
<th class="text-end">Qty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var materialResult in packResult.MaterialResults.Where(mr => mr.ToBePurchasedBins.Count > 0))
|
||||
{
|
||||
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
|
||||
{
|
||||
<tr>
|
||||
<td>@materialResult.Material.DisplayName</td>
|
||||
<td>@ArchUnits.FormatFromInches(group.Key)</td>
|
||||
<td class="text-end">@group.Count()</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="fw-bold">
|
||||
<td colspan="2">Total</td>
|
||||
<td class="text-end">@summary.TotalToBePurchasedBins bars</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cut Lists by Material -->
|
||||
@foreach (var materialResult in packResult.MaterialResults)
|
||||
{
|
||||
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
|
||||
var allBins = materialResult.InStockBins
|
||||
.Select(b => new { Bin = b, Source = "Stock" })
|
||||
.Concat(materialResult.ToBePurchasedBins
|
||||
.Select(b => new { Bin = b, Source = "Purchase" }))
|
||||
.ToList();
|
||||
|
||||
<div class="card mb-4 cutlist-material-card">
|
||||
<div class="card-header cutlist-material-screen-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">@materialResult.Material.DisplayName</h5>
|
||||
<span class="text-muted">
|
||||
@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins) bars
|
||||
· @materialSummary.TotalPieces pieces
|
||||
· @materialSummary.Efficiency.ToString("F1")% efficiency
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
|
||||
No stock lengths available or parts too long.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr class="cutlist-material-print-header">
|
||||
<th colspan="5">
|
||||
<span class="cutlist-material-name">@materialResult.Material.DisplayName</span>
|
||||
<span class="cutlist-material-stats">
|
||||
@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins) bars
|
||||
· @materialSummary.TotalPieces pieces
|
||||
· @materialSummary.Efficiency.ToString("F1")% efficiency
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th style="width: 90px;">Source</th>
|
||||
<th style="white-space: nowrap;">Stock Length</th>
|
||||
<th>Cuts</th>
|
||||
<th style="width: 120px; white-space: nowrap;">Waste</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var binNum = 1; }
|
||||
@foreach (var entry in allBins)
|
||||
{
|
||||
<tr>
|
||||
<td>@binNum</td>
|
||||
<td>
|
||||
@if (entry.Source == "Stock")
|
||||
{
|
||||
<span class="badge bg-success">Stock</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Purchase</span>
|
||||
}
|
||||
</td>
|
||||
<td style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.Length)</td>
|
||||
<td>
|
||||
@foreach (var item in entry.Bin.Items)
|
||||
{
|
||||
<span class="badge bg-primary me-1">
|
||||
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.RemainingLength)</td>
|
||||
</tr>
|
||||
binNum++;
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (!optimizing)
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-scissors display-4"></i>
|
||||
<p class="mt-3">Click <strong>Optimize</strong> to calculate the most efficient cut list.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private async Task RunOptimization()
|
||||
{
|
||||
optimizing = true;
|
||||
try
|
||||
{
|
||||
var kerf = job.CuttingTool?.KerfInches ?? 0.125m;
|
||||
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
||||
summary = PackingService.GetSummary(packResult);
|
||||
|
||||
// Save to database
|
||||
var json = PackingService.SerializeResult(packResult);
|
||||
await JobService.SaveOptimizationResultAsync(Id!.Value, json, DateTime.UtcNow);
|
||||
|
||||
// Refresh job to get updated OptimizedAt
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
addedToOrderList = job.IsLocked;
|
||||
}
|
||||
finally
|
||||
{
|
||||
optimizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddToOrderList()
|
||||
{
|
||||
addingToOrderList = true;
|
||||
try
|
||||
{
|
||||
var purchaseItems = new List<PurchaseItem>();
|
||||
var stockItems = await StockItemService.GetAllAsync();
|
||||
|
||||
foreach (var materialResult in packResult!.MaterialResults)
|
||||
{
|
||||
if (materialResult.ToBePurchasedBins.Count == 0) continue;
|
||||
|
||||
var materialId = materialResult.Material.Id;
|
||||
|
||||
// Group bins by length to consolidate quantities
|
||||
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
|
||||
{
|
||||
var lengthInches = (decimal)group.Key;
|
||||
var quantity = group.Count();
|
||||
|
||||
// Find the matching stock item
|
||||
var stockItem = stockItems.FirstOrDefault(s =>
|
||||
s.MaterialId == materialId && s.LengthInches == lengthInches);
|
||||
|
||||
if (stockItem != null)
|
||||
{
|
||||
purchaseItems.Add(new PurchaseItem
|
||||
{
|
||||
StockItemId = stockItem.Id,
|
||||
Quantity = quantity,
|
||||
JobId = Id!.Value,
|
||||
Status = PurchaseItemStatus.Pending
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (purchaseItems.Count > 0)
|
||||
{
|
||||
await PurchaseItemService.CreateBulkAsync(purchaseItems);
|
||||
}
|
||||
|
||||
await JobService.LockAsync(Id!.Value);
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
addedToOrderList = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
addingToOrderList = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrintReport()
|
||||
{
|
||||
var filename = $"CutList - {job.Name} - {DateTime.Now:yyyy-MM-dd}";
|
||||
await JS.InvokeVoidAsync("printWithTitle", filename);
|
||||
}
|
||||
|
||||
private void ShowAddCustomStock()
|
||||
@@ -911,6 +1398,8 @@ else
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
showStockForm = false;
|
||||
editingStock = null;
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
|
||||
private async Task SaveCustomStockAsync()
|
||||
@@ -956,12 +1445,16 @@ else
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
showCustomStockForm = false;
|
||||
editingStock = null;
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
|
||||
private async Task DeleteStock(JobStock stock)
|
||||
{
|
||||
await JobService.DeleteStockAsync(stock.Id);
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
|
||||
// Import modal methods
|
||||
@@ -1048,6 +1541,8 @@ else
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
showImportModal = false;
|
||||
importCandidates.Clear();
|
||||
packResult = null;
|
||||
summary = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1055,6 +1550,13 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private class PartRow
|
||||
{
|
||||
public decimal LengthInches { get; set; }
|
||||
public int Quantity { get; set; } = 1;
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
private class ImportStockCandidate
|
||||
{
|
||||
public StockItem StockItem { get; set; } = null!;
|
||||
|
||||
@@ -63,7 +63,6 @@ else
|
||||
<td>@((job.UpdatedAt ?? job.CreatedAt).ToLocalTime().ToString("g"))</td>
|
||||
<td>
|
||||
<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" title="Optimize"><i class="bi bi-scissors"></i></a>
|
||||
<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)" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
@page "/jobs/{Id:int}/results"
|
||||
@inject JobService JobService
|
||||
@inject CutListPackingService PackingService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
@inject PurchaseItemService PurchaseItemService
|
||||
@inject StockItemService StockItemService
|
||||
@using CutList.Core
|
||||
@using CutList.Core.Nesting
|
||||
@using CutList.Core.Formatting
|
||||
|
||||
<PageTitle>Results - @(job?.DisplayName ?? "Job")</PageTitle>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else if (job == null)
|
||||
{
|
||||
<div class="alert alert-danger">Job not found.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1>
|
||||
@job.DisplayName
|
||||
@if (job.IsLocked)
|
||||
{
|
||||
<i class="bi bi-lock-fill text-warning ms-2" title="Job locked — materials ordered"></i>
|
||||
}
|
||||
</h1>
|
||||
@if (!string.IsNullOrWhiteSpace(job.Customer))
|
||||
{
|
||||
<p class="text-muted mb-0">Customer: @job.Customer</p>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<a href="jobs/@Id" class="btn btn-outline-secondary me-2">Edit Job</a>
|
||||
<button class="btn btn-primary" @onclick="PrintReport">Print Report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!CanOptimize)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<h4>Cannot Optimize</h4>
|
||||
<ul class="mb-0">
|
||||
@if (job.Parts.Count == 0)
|
||||
{
|
||||
<li>No parts defined. <a href="jobs/@Id">Add parts to the job</a>.</li>
|
||||
}
|
||||
@if (job.CuttingToolId == null)
|
||||
{
|
||||
<li>No cutting tool selected. <a href="jobs/@Id">Select a cutting tool</a>.</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else if (packResult != null)
|
||||
{
|
||||
@if (summary!.TotalItemsNotPlaced > 0)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<h5>Items Not Placed</h5>
|
||||
<p>Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Overall Summary Cards -->
|
||||
<div class="row mb-4 print-summary">
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
|
||||
<p class="card-text text-muted">Total Stock Bars</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@summary.TotalPieces</h2>
|
||||
<p class="card-text text-muted">Total Pieces</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
|
||||
<p class="card-text text-muted">Total Waste</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-0">@summary.Efficiency.ToString("F1")%</h2>
|
||||
<p class="card-text text-muted">Efficiency</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock Summary -->
|
||||
<div class="row mb-4 print-stock-summary">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">In Stock</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>@summary.TotalInStockBins bars</h3>
|
||||
<p class="text-muted mb-0">Ready to cut from existing inventory</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning">
|
||||
<h5 class="mb-0">To Be Purchased</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results by Material -->
|
||||
@foreach (var materialResult in packResult.MaterialResults)
|
||||
{
|
||||
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">@materialResult.Material.DisplayName</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Material Summary -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-4">
|
||||
<strong>@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins)</strong> bars
|
||||
</div>
|
||||
<div class="col-md-2 col-4">
|
||||
<strong>@materialSummary.TotalPieces</strong> pieces
|
||||
</div>
|
||||
<div class="col-md-2 col-4">
|
||||
<strong>@materialSummary.Efficiency.ToString("F1")%</strong> efficiency
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<span class="text-success">@materialSummary.InStockBins in stock</span>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<span class="text-warning">@materialSummary.ToBePurchasedBins to purchase</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> -
|
||||
No stock lengths available or parts too long.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (materialResult.InStockBins.Count > 0)
|
||||
{
|
||||
<h5 class="text-success mt-3">In Stock (@materialResult.InStockBins.Count bars)</h5>
|
||||
@RenderBinList(materialResult.InStockBins)
|
||||
}
|
||||
|
||||
@if (materialResult.ToBePurchasedBins.Count > 0)
|
||||
{
|
||||
<h5 class="text-warning mt-3">To Be Purchased (@materialResult.ToBePurchasedBins.Count bars)</h5>
|
||||
@RenderBinList(materialResult.ToBePurchasedBins)
|
||||
|
||||
<!-- Purchase Summary -->
|
||||
<div class="mt-3 p-3 bg-light rounded">
|
||||
<strong>Order Summary:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
|
||||
{
|
||||
<li>@group.Count() x @ArchUnits.FormatFromInches(group.Key)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private Job? job;
|
||||
private MultiMaterialPackResult? packResult;
|
||||
private MultiMaterialPackingSummary? summary;
|
||||
private bool loading = true;
|
||||
|
||||
private bool addingToOrderList;
|
||||
private bool addedToOrderList;
|
||||
|
||||
private bool CanOptimize => job != null &&
|
||||
job.Parts.Count > 0 &&
|
||||
job.CuttingToolId != null;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
job = await JobService.GetByIdAsync(Id);
|
||||
|
||||
if (job != null && CanOptimize)
|
||||
{
|
||||
var kerf = job.CuttingTool?.KerfInches ?? 0.125m;
|
||||
// Pass job stock if configured, otherwise packing service uses all available stock
|
||||
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
||||
summary = PackingService.GetSummary(packResult);
|
||||
addedToOrderList = job.IsLocked;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private RenderFragment RenderBinList(List<Bin> bins) => __builder =>
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">#</th>
|
||||
<th>Stock Length</th>
|
||||
<th>Cuts</th>
|
||||
<th>Waste</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var binNumber = 1; }
|
||||
@foreach (var bin in bins)
|
||||
{
|
||||
<tr>
|
||||
<td>@binNumber</td>
|
||||
<td>@ArchUnits.FormatFromInches(bin.Length)</td>
|
||||
<td>
|
||||
@foreach (var item in bin.Items)
|
||||
{
|
||||
<span class="badge bg-primary me-1">
|
||||
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>@ArchUnits.FormatFromInches(bin.RemainingLength)</td>
|
||||
</tr>
|
||||
binNumber++;
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task AddToOrderList()
|
||||
{
|
||||
addingToOrderList = true;
|
||||
try
|
||||
{
|
||||
var purchaseItems = new List<PurchaseItem>();
|
||||
var stockItems = await StockItemService.GetAllAsync();
|
||||
|
||||
foreach (var materialResult in packResult!.MaterialResults)
|
||||
{
|
||||
if (materialResult.ToBePurchasedBins.Count == 0) continue;
|
||||
|
||||
var materialId = materialResult.Material.Id;
|
||||
|
||||
// Group bins by length to consolidate quantities
|
||||
foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length))
|
||||
{
|
||||
var lengthInches = (decimal)group.Key;
|
||||
var quantity = group.Count();
|
||||
|
||||
// Find the matching stock item
|
||||
var stockItem = stockItems.FirstOrDefault(s =>
|
||||
s.MaterialId == materialId && s.LengthInches == lengthInches);
|
||||
|
||||
if (stockItem != null)
|
||||
{
|
||||
purchaseItems.Add(new PurchaseItem
|
||||
{
|
||||
StockItemId = stockItem.Id,
|
||||
Quantity = quantity,
|
||||
JobId = Id,
|
||||
Status = PurchaseItemStatus.Pending
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (purchaseItems.Count > 0)
|
||||
{
|
||||
await PurchaseItemService.CreateBulkAsync(purchaseItems);
|
||||
}
|
||||
|
||||
await JobService.LockAsync(Id);
|
||||
job = await JobService.GetByIdAsync(Id);
|
||||
addedToOrderList = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
addingToOrderList = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrintReport()
|
||||
{
|
||||
var filename = $"CutList - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
||||
await JS.InvokeVoidAsync("printWithTitle", filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CatalogController : ControllerBase
|
||||
{
|
||||
private readonly CatalogService _catalogService;
|
||||
|
||||
public CatalogController(CatalogService catalogService)
|
||||
{
|
||||
_catalogService = catalogService;
|
||||
}
|
||||
|
||||
[HttpGet("export")]
|
||||
public async Task<IActionResult> Export()
|
||||
{
|
||||
var data = await _catalogService.ExportAsync();
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
return new JsonResult(data, options);
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<ImportResultDto>> Import([FromBody] CatalogData data)
|
||||
{
|
||||
var result = await _catalogService.ImportAsync(data);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SeedController : ControllerBase
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public SeedController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpPost("alro-1018-round")]
|
||||
public async Task<ActionResult> SeedAlro1018Round()
|
||||
{
|
||||
// Add Alro supplier if not exists
|
||||
var alro = await _context.Suppliers.FirstOrDefaultAsync(s => s.Name == "Alro");
|
||||
if (alro == null)
|
||||
{
|
||||
alro = new Supplier
|
||||
{
|
||||
Name = "Alro",
|
||||
ContactInfo = "https://www.alro.com",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
_context.Suppliers.Add(alro);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 1018 CF Round bar sizes from the screenshot
|
||||
var sizes = new[]
|
||||
{
|
||||
"1/8\"",
|
||||
"5/32\"",
|
||||
"3/16\"",
|
||||
"7/32\"",
|
||||
".236\"",
|
||||
"1/4\"",
|
||||
"9/32\"",
|
||||
"5/16\"",
|
||||
"11/32\"",
|
||||
"3/8\"",
|
||||
".394\"",
|
||||
"13/32\"",
|
||||
"7/16\"",
|
||||
"15/32\"",
|
||||
".472\"",
|
||||
"1/2\"",
|
||||
"17/32\"",
|
||||
"9/16\"",
|
||||
".593\""
|
||||
};
|
||||
|
||||
var created = 0;
|
||||
var skipped = 0;
|
||||
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
var exists = await _context.Materials
|
||||
.AnyAsync(m => m.Shape == MaterialShape.RoundBar && m.Size == size && m.IsActive);
|
||||
|
||||
if (exists)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_context.Materials.Add(new Material
|
||||
{
|
||||
Shape = MaterialShape.RoundBar,
|
||||
Size = size,
|
||||
Description = "1018 Cold Finished",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
created++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = "Alro 1018 CF Round materials seeded",
|
||||
SupplierId = alro.Id,
|
||||
MaterialsCreated = created,
|
||||
MaterialsSkipped = skipped
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class CatalogData
|
||||
{
|
||||
public DateTime ExportedAt { get; set; }
|
||||
public List<CatalogSupplierDto> Suppliers { get; set; } = [];
|
||||
public List<CatalogCuttingToolDto> CuttingTools { get; set; } = [];
|
||||
public CatalogMaterialsDto Materials { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CatalogSupplierDto
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string? ContactInfo { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogCuttingToolDto
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogMaterialsDto
|
||||
{
|
||||
public List<CatalogAngleDto> Angles { get; set; } = [];
|
||||
public List<CatalogChannelDto> Channels { get; set; } = [];
|
||||
public List<CatalogFlatBarDto> FlatBars { get; set; } = [];
|
||||
public List<CatalogIBeamDto> IBeams { get; set; } = [];
|
||||
public List<CatalogPipeDto> Pipes { get; set; } = [];
|
||||
public List<CatalogRectangularTubeDto> RectangularTubes { get; set; } = [];
|
||||
public List<CatalogRoundBarDto> RoundBars { get; set; } = [];
|
||||
public List<CatalogRoundTubeDto> RoundTubes { get; set; } = [];
|
||||
public List<CatalogSquareBarDto> SquareBars { get; set; } = [];
|
||||
public List<CatalogSquareTubeDto> SquareTubes { get; set; } = [];
|
||||
}
|
||||
|
||||
public abstract class CatalogMaterialBaseDto
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public string? Grade { get; set; }
|
||||
public string Size { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public List<CatalogStockItemDto> StockItems { get; set; } = [];
|
||||
}
|
||||
|
||||
public class CatalogAngleDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Leg1 { get; set; }
|
||||
public decimal Leg2 { get; set; }
|
||||
public decimal Thickness { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogChannelDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Height { get; set; }
|
||||
public decimal Flange { get; set; }
|
||||
public decimal Web { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogFlatBarDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Width { get; set; }
|
||||
public decimal Thickness { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogIBeamDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Height { get; set; }
|
||||
public decimal WeightPerFoot { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogPipeDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal NominalSize { get; set; }
|
||||
public decimal Wall { get; set; }
|
||||
public string? Schedule { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogRectangularTubeDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Width { get; set; }
|
||||
public decimal Height { get; set; }
|
||||
public decimal Wall { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogRoundBarDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal Diameter { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogRoundTubeDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal OuterDiameter { get; set; }
|
||||
public decimal Wall { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogSquareBarDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal SideLength { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogSquareTubeDto : CatalogMaterialBaseDto
|
||||
{
|
||||
public decimal SideLength { get; set; }
|
||||
public decimal Wall { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogStockItemDto
|
||||
{
|
||||
public decimal LengthInches { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public int QuantityOnHand { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public List<CatalogSupplierOfferingDto> SupplierOfferings { get; set; } = [];
|
||||
}
|
||||
|
||||
public class CatalogSupplierOfferingDto
|
||||
{
|
||||
public string SupplierName { get; set; } = "";
|
||||
public string? PartNumber { get; set; }
|
||||
public string? SupplierDescription { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class ImportResultDto
|
||||
{
|
||||
public int SuppliersCreated { get; set; }
|
||||
public int SuppliersUpdated { get; set; }
|
||||
public int CuttingToolsCreated { get; set; }
|
||||
public int CuttingToolsUpdated { get; set; }
|
||||
public int MaterialsCreated { get; set; }
|
||||
public int MaterialsUpdated { get; set; }
|
||||
public int StockItemsCreated { get; set; }
|
||||
public int StockItemsUpdated { get; set; }
|
||||
public int OfferingsCreated { get; set; }
|
||||
public int OfferingsUpdated { get; set; }
|
||||
public List<string> Errors { get; set; } = [];
|
||||
public List<string> Warnings { get; set; } = [];
|
||||
}
|
||||
@@ -47,84 +47,80 @@ public class ApplicationDbContext : DbContext
|
||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
||||
});
|
||||
|
||||
// MaterialDimensions - TPH inheritance
|
||||
// MaterialDimensions - TPC inheritance (each shape gets its own table, no base table)
|
||||
modelBuilder.Entity<MaterialDimensions>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.UseTpcMappingStrategy();
|
||||
|
||||
// 1:1 relationship with Material
|
||||
entity.HasOne(e => e.Material)
|
||||
.WithOne(m => m.Dimensions)
|
||||
.HasForeignKey<MaterialDimensions>(e => e.MaterialId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// TPH discriminator
|
||||
entity.HasDiscriminator<string>("DimensionType")
|
||||
.HasValue<RoundBarDimensions>("RoundBar")
|
||||
.HasValue<RoundTubeDimensions>("RoundTube")
|
||||
.HasValue<FlatBarDimensions>("FlatBar")
|
||||
.HasValue<SquareBarDimensions>("SquareBar")
|
||||
.HasValue<SquareTubeDimensions>("SquareTube")
|
||||
.HasValue<RectangularTubeDimensions>("RectangularTube")
|
||||
.HasValue<AngleDimensions>("Angle")
|
||||
.HasValue<ChannelDimensions>("Channel")
|
||||
.HasValue<IBeamDimensions>("IBeam")
|
||||
.HasValue<PipeDimensions>("Pipe");
|
||||
});
|
||||
|
||||
// Configure each dimension type's properties
|
||||
modelBuilder.Entity<RoundBarDimensions>(entity =>
|
||||
{
|
||||
entity.ToTable("DimRoundBar");
|
||||
entity.Property(e => e.Diameter).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Diameter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RoundTubeDimensions>(entity =>
|
||||
{
|
||||
entity.ToTable("DimRoundTube");
|
||||
entity.Property(e => e.OuterDiameter).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.OuterDiameter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FlatBarDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Width).HasColumnName("Width").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Thickness).HasColumnName("Thickness").HasPrecision(10, 4);
|
||||
entity.ToTable("DimFlatBar");
|
||||
entity.Property(e => e.Width).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Thickness).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Width);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SquareBarDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Size).HasColumnName("Size").HasPrecision(10, 4);
|
||||
entity.ToTable("DimSquareBar");
|
||||
entity.Property(e => e.Size).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Size);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SquareTubeDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Size).HasColumnName("Size").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
|
||||
entity.ToTable("DimSquareTube");
|
||||
entity.Property(e => e.Size).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Size);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RectangularTubeDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Width).HasColumnName("Width").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
|
||||
entity.ToTable("DimRectangularTube");
|
||||
entity.Property(e => e.Width).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Height).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Width);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AngleDimensions>(entity =>
|
||||
{
|
||||
entity.ToTable("DimAngle");
|
||||
entity.Property(e => e.Leg1).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Leg2).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Thickness).HasColumnName("Thickness").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Thickness).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Leg1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ChannelDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
|
||||
entity.ToTable("DimChannel");
|
||||
entity.Property(e => e.Height).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Flange).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Web).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Height);
|
||||
@@ -132,15 +128,17 @@ public class ApplicationDbContext : DbContext
|
||||
|
||||
modelBuilder.Entity<IBeamDimensions>(entity =>
|
||||
{
|
||||
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
|
||||
entity.ToTable("DimIBeam");
|
||||
entity.Property(e => e.Height).HasPrecision(10, 4);
|
||||
entity.Property(e => e.WeightPerFoot).HasPrecision(10, 4);
|
||||
entity.HasIndex(e => e.Height);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PipeDimensions>(entity =>
|
||||
{
|
||||
entity.ToTable("DimPipe");
|
||||
entity.Property(e => e.NominalSize).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
|
||||
entity.Property(e => e.Wall).HasPrecision(10, 4);
|
||||
entity.Property(e => e.Schedule).HasMaxLength(20);
|
||||
entity.HasIndex(e => e.NominalSize);
|
||||
});
|
||||
@@ -234,6 +232,8 @@ public class ApplicationDbContext : DbContext
|
||||
entity.Property(e => e.Customer).HasMaxLength(100);
|
||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
entity.Property(e => e.OptimizationResultJson).HasColumnType("nvarchar(max)");
|
||||
|
||||
entity.HasIndex(e => e.JobNumber).IsUnique();
|
||||
|
||||
entity.HasOne(e => e.CuttingTool)
|
||||
|
||||
@@ -11,6 +11,8 @@ public class Job
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public DateTime? LockedAt { get; set; }
|
||||
public string? OptimizationResultJson { get; set; }
|
||||
public DateTime? OptimizedAt { get; set; }
|
||||
|
||||
public bool IsLocked => LockedAt.HasValue;
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"exportedAt": "2026-02-16T17:09:52.843008+00:00",
|
||||
"suppliers": [
|
||||
{
|
||||
"name": "Alro Steel"
|
||||
}
|
||||
],
|
||||
"cuttingTools": [
|
||||
{
|
||||
"name": "Bandsaw",
|
||||
"kerfInches": 0.0625,
|
||||
"isDefault": true
|
||||
},
|
||||
{
|
||||
"name": "Chop Saw",
|
||||
"kerfInches": 0.125,
|
||||
"isDefault": false
|
||||
},
|
||||
{
|
||||
"name": "Cold Cut Saw",
|
||||
"kerfInches": 0.0625,
|
||||
"isDefault": false
|
||||
},
|
||||
{
|
||||
"name": "Hacksaw",
|
||||
"kerfInches": 0.0625,
|
||||
"isDefault": false
|
||||
}
|
||||
],
|
||||
"materials": {
|
||||
"angles": [],
|
||||
"channels": [],
|
||||
"flatBars": [],
|
||||
"iBeams": [],
|
||||
"pipes": [],
|
||||
"rectangularTubes": [],
|
||||
"roundBars": [],
|
||||
"roundTubes": [],
|
||||
"squareBars": [],
|
||||
"squareTubes": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,905 @@
|
||||
// <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("20260209122312_AddJobOptimizationResult")]
|
||||
partial class AddJobOptimizationResult
|
||||
{
|
||||
/// <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<string>("OptimizationResultJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("OptimizedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CuttingToolId");
|
||||
|
||||
b.HasIndex("JobNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.ToTable("JobParts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsCustomLength")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("StockItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.HasIndex("StockItemId");
|
||||
|
||||
b.ToTable("JobStocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("Grade")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Shape")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Size")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Materials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
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,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CutList.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddJobOptimizationResult : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OptimizationResultJson",
|
||||
table: "Jobs",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "OptimizedAt",
|
||||
table: "Jobs",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OptimizationResultJson",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OptimizedAt",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+962
@@ -0,0 +1,962 @@
|
||||
// <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("20260216183131_MaterialDimensionsTPHtoTPT")]
|
||||
partial class MaterialDimensionsTPHtoTPT
|
||||
{
|
||||
/// <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<string>("OptimizationResultJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("OptimizedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CuttingToolId");
|
||||
|
||||
b.HasIndex("JobNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.ToTable("JobParts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsCustomLength")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("StockItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.HasIndex("StockItemId");
|
||||
|
||||
b.ToTable("JobStocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("Grade")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Shape")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Size")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Materials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MaterialId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MaterialDimensions");
|
||||
|
||||
b.UseTptMappingStrategy();
|
||||
});
|
||||
|
||||
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")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Leg1");
|
||||
|
||||
b.ToTable("AngleDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Flange")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Web")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("ChannelDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Thickness")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("FlatBarDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("WeightPerFoot")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("IBeamDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("NominalSize")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<string>("Schedule")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<decimal?>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("NominalSize");
|
||||
|
||||
b.ToTable("PipeDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("RectangularTubeDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Diameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Diameter");
|
||||
|
||||
b.ToTable("RoundBarDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("OuterDiameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("OuterDiameter");
|
||||
|
||||
b.ToTable("RoundTubeDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("SquareBarDimensions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("SquareTubeDimensions");
|
||||
});
|
||||
|
||||
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.AngleDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.AngleDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.ChannelDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.FlatBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.IBeamDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.PipeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RectangularTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RoundBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RoundTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.SquareBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.SquareTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
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,353 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CutList.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MaterialDimensionsTPHtoTPT : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1. Create the new TPT tables first (before dropping any columns)
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AngleDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Leg1 = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Leg2 = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Thickness = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AngleDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AngleDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChannelDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Flange = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Web = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChannelDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChannelDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FlatBarDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Width = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Thickness = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FlatBarDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IBeamDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
WeightPerFoot = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IBeamDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IBeamDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PipeDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
NominalSize = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: true),
|
||||
Schedule = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PipeDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PipeDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RectangularTubeDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Width = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RectangularTubeDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RoundBarDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Diameter = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RoundBarDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RoundTubeDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
OuterDiameter = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RoundTubeDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SquareBarDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Size = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SquareBarDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SquareTubeDimensions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Size = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SquareTubeDimensions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
// 2. Migrate existing data from the TPH table into the new TPT tables
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO RoundBarDimensions (Id, Diameter)
|
||||
SELECT Id, ISNULL(Diameter, 0) FROM MaterialDimensions WHERE DimensionType = 'RoundBar';
|
||||
|
||||
INSERT INTO RoundTubeDimensions (Id, OuterDiameter, Wall)
|
||||
SELECT Id, ISNULL(OuterDiameter, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'RoundTube';
|
||||
|
||||
INSERT INTO FlatBarDimensions (Id, Width, Thickness)
|
||||
SELECT Id, ISNULL(Width, 0), ISNULL(Thickness, 0) FROM MaterialDimensions WHERE DimensionType = 'FlatBar';
|
||||
|
||||
INSERT INTO SquareBarDimensions (Id, Size)
|
||||
SELECT Id, ISNULL(Size, 0) FROM MaterialDimensions WHERE DimensionType = 'SquareBar';
|
||||
|
||||
INSERT INTO SquareTubeDimensions (Id, Size, Wall)
|
||||
SELECT Id, ISNULL(Size, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'SquareTube';
|
||||
|
||||
INSERT INTO RectangularTubeDimensions (Id, Width, Height, Wall)
|
||||
SELECT Id, ISNULL(Width, 0), ISNULL(Height, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'RectangularTube';
|
||||
|
||||
INSERT INTO AngleDimensions (Id, Leg1, Leg2, Thickness)
|
||||
SELECT Id, ISNULL(Leg1, 0), ISNULL(Leg2, 0), ISNULL(Thickness, 0) FROM MaterialDimensions WHERE DimensionType = 'Angle';
|
||||
|
||||
INSERT INTO ChannelDimensions (Id, Height, Flange, Web)
|
||||
SELECT Id, ISNULL(Height, 0), ISNULL(Flange, 0), ISNULL(Web, 0) FROM MaterialDimensions WHERE DimensionType = 'Channel';
|
||||
|
||||
INSERT INTO IBeamDimensions (Id, Height, WeightPerFoot)
|
||||
SELECT Id, ISNULL(Height, 0), ISNULL(WeightPerFoot, 0) FROM MaterialDimensions WHERE DimensionType = 'IBeam';
|
||||
|
||||
INSERT INTO PipeDimensions (Id, NominalSize, Wall, Schedule)
|
||||
SELECT Id, ISNULL(NominalSize, 0), Wall, Schedule FROM MaterialDimensions WHERE DimensionType = 'Pipe';
|
||||
");
|
||||
|
||||
// 3. Now drop the old TPH columns and indexes
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_Diameter",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_Height",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_Leg1",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_NominalSize",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_OuterDiameter",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_Size",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MaterialDimensions_Width",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropColumn(name: "Diameter", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "DimensionType", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Flange", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Height", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Leg1", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Leg2", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "NominalSize", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "OuterDiameter", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Schedule", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Size", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Thickness", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Wall", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Web", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "WeightPerFoot", table: "MaterialDimensions");
|
||||
migrationBuilder.DropColumn(name: "Width", table: "MaterialDimensions");
|
||||
|
||||
// 4. Create indexes on the new tables
|
||||
migrationBuilder.CreateIndex(name: "IX_AngleDimensions_Leg1", table: "AngleDimensions", column: "Leg1");
|
||||
migrationBuilder.CreateIndex(name: "IX_ChannelDimensions_Height", table: "ChannelDimensions", column: "Height");
|
||||
migrationBuilder.CreateIndex(name: "IX_FlatBarDimensions_Width", table: "FlatBarDimensions", column: "Width");
|
||||
migrationBuilder.CreateIndex(name: "IX_IBeamDimensions_Height", table: "IBeamDimensions", column: "Height");
|
||||
migrationBuilder.CreateIndex(name: "IX_PipeDimensions_NominalSize", table: "PipeDimensions", column: "NominalSize");
|
||||
migrationBuilder.CreateIndex(name: "IX_RectangularTubeDimensions_Width", table: "RectangularTubeDimensions", column: "Width");
|
||||
migrationBuilder.CreateIndex(name: "IX_RoundBarDimensions_Diameter", table: "RoundBarDimensions", column: "Diameter");
|
||||
migrationBuilder.CreateIndex(name: "IX_RoundTubeDimensions_OuterDiameter", table: "RoundTubeDimensions", column: "OuterDiameter");
|
||||
migrationBuilder.CreateIndex(name: "IX_SquareBarDimensions_Size", table: "SquareBarDimensions", column: "Size");
|
||||
migrationBuilder.CreateIndex(name: "IX_SquareTubeDimensions_Size", table: "SquareTubeDimensions", column: "Size");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Re-add the TPH columns
|
||||
migrationBuilder.AddColumn<string>(name: "DimensionType", table: "MaterialDimensions", type: "nvarchar(21)", maxLength: 21, nullable: false, defaultValue: "");
|
||||
migrationBuilder.AddColumn<decimal>(name: "Diameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Flange", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Height", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Leg1", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Leg2", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "NominalSize", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "OuterDiameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(name: "Schedule", table: "MaterialDimensions", type: "nvarchar(20)", maxLength: 20, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Size", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Thickness", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Wall", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Web", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "WeightPerFoot", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
migrationBuilder.AddColumn<decimal>(name: "Width", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
|
||||
|
||||
// Migrate data back to TPH
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE md SET DimensionType = 'RoundBar', Diameter = rb.Diameter FROM MaterialDimensions md INNER JOIN RoundBarDimensions rb ON md.Id = rb.Id;
|
||||
UPDATE md SET DimensionType = 'RoundTube', OuterDiameter = rt.OuterDiameter, Wall = rt.Wall FROM MaterialDimensions md INNER JOIN RoundTubeDimensions rt ON md.Id = rt.Id;
|
||||
UPDATE md SET DimensionType = 'FlatBar', Width = fb.Width, Thickness = fb.Thickness FROM MaterialDimensions md INNER JOIN FlatBarDimensions fb ON md.Id = fb.Id;
|
||||
UPDATE md SET DimensionType = 'SquareBar', Size = sb.Size FROM MaterialDimensions md INNER JOIN SquareBarDimensions sb ON md.Id = sb.Id;
|
||||
UPDATE md SET DimensionType = 'SquareTube', Size = st.Size, Wall = st.Wall FROM MaterialDimensions md INNER JOIN SquareTubeDimensions st ON md.Id = st.Id;
|
||||
UPDATE md SET DimensionType = 'RectangularTube', Width = rt.Width, Height = rt.Height, Wall = rt.Wall FROM MaterialDimensions md INNER JOIN RectangularTubeDimensions rt ON md.Id = rt.Id;
|
||||
UPDATE md SET DimensionType = 'Angle', Leg1 = a.Leg1, Leg2 = a.Leg2, Thickness = a.Thickness FROM MaterialDimensions md INNER JOIN AngleDimensions a ON md.Id = a.Id;
|
||||
UPDATE md SET DimensionType = 'Channel', Height = c.Height, Flange = c.Flange, Web = c.Web FROM MaterialDimensions md INNER JOIN ChannelDimensions c ON md.Id = c.Id;
|
||||
UPDATE md SET DimensionType = 'IBeam', Height = ib.Height, WeightPerFoot = ib.WeightPerFoot FROM MaterialDimensions md INNER JOIN IBeamDimensions ib ON md.Id = ib.Id;
|
||||
UPDATE md SET DimensionType = 'Pipe', NominalSize = p.NominalSize, Wall = p.Wall, Schedule = p.Schedule FROM MaterialDimensions md INNER JOIN PipeDimensions p ON md.Id = p.Id;
|
||||
");
|
||||
|
||||
// Drop TPT tables
|
||||
migrationBuilder.DropTable(name: "AngleDimensions");
|
||||
migrationBuilder.DropTable(name: "ChannelDimensions");
|
||||
migrationBuilder.DropTable(name: "FlatBarDimensions");
|
||||
migrationBuilder.DropTable(name: "IBeamDimensions");
|
||||
migrationBuilder.DropTable(name: "PipeDimensions");
|
||||
migrationBuilder.DropTable(name: "RectangularTubeDimensions");
|
||||
migrationBuilder.DropTable(name: "RoundBarDimensions");
|
||||
migrationBuilder.DropTable(name: "RoundTubeDimensions");
|
||||
migrationBuilder.DropTable(name: "SquareBarDimensions");
|
||||
migrationBuilder.DropTable(name: "SquareTubeDimensions");
|
||||
|
||||
// Re-create TPH indexes
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Diameter", table: "MaterialDimensions", column: "Diameter");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Height", table: "MaterialDimensions", column: "Height");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Leg1", table: "MaterialDimensions", column: "Leg1");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_NominalSize", table: "MaterialDimensions", column: "NominalSize");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_OuterDiameter", table: "MaterialDimensions", column: "OuterDiameter");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Size", table: "MaterialDimensions", column: "Size");
|
||||
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Width", table: "MaterialDimensions", column: "Width");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,962 @@
|
||||
// <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("20260216190925_RenameDimensionTables")]
|
||||
partial class RenameDimensionTables
|
||||
{
|
||||
/// <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<string>("OptimizationResultJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("OptimizedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CuttingToolId");
|
||||
|
||||
b.HasIndex("JobNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.ToTable("JobParts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsCustomLength")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("StockItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.HasIndex("StockItemId");
|
||||
|
||||
b.ToTable("JobStocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("Grade")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Shape")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Size")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Materials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MaterialId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DimBase", (string)null);
|
||||
|
||||
b.UseTptMappingStrategy();
|
||||
});
|
||||
|
||||
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")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Leg1");
|
||||
|
||||
b.ToTable("DimAngle", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Flange")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Web")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("DimChannel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Thickness")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("DimFlatBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("WeightPerFoot")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("DimIBeam", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("NominalSize")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<string>("Schedule")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<decimal?>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("NominalSize");
|
||||
|
||||
b.ToTable("DimPipe", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("DimRectangularTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Diameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Diameter");
|
||||
|
||||
b.ToTable("DimRoundBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("OuterDiameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("OuterDiameter");
|
||||
|
||||
b.ToTable("DimRoundTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("DimSquareBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("DimSquareTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("CuttingToolId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("CuttingTool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||
.WithMany("Parts")
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithMany("JobParts")
|
||||
.HasForeignKey("MaterialId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Job");
|
||||
|
||||
b.Navigation("Material");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||
.WithMany("Stock")
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithMany()
|
||||
.HasForeignKey("MaterialId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("StockItemId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Job");
|
||||
|
||||
b.Navigation("Material");
|
||||
|
||||
b.Navigation("StockItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithOne("Dimensions")
|
||||
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Material");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.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.AngleDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.AngleDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.ChannelDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.FlatBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.IBeamDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.PipeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RectangularTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RoundBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.RoundTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.SquareBarDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("CutList.Web.Data.Entities.SquareTubeDimensions", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
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,678 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CutList.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameDimensionTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AngleDimensions_MaterialDimensions_Id",
|
||||
table: "AngleDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ChannelDimensions_MaterialDimensions_Id",
|
||||
table: "ChannelDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
|
||||
table: "FlatBarDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_IBeamDimensions_MaterialDimensions_Id",
|
||||
table: "IBeamDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MaterialDimensions_Materials_MaterialId",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PipeDimensions_MaterialDimensions_Id",
|
||||
table: "PipeDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
|
||||
table: "RectangularTubeDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
|
||||
table: "RoundBarDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
|
||||
table: "RoundTubeDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
|
||||
table: "SquareBarDimensions");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
|
||||
table: "SquareTubeDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_SquareTubeDimensions",
|
||||
table: "SquareTubeDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_SquareBarDimensions",
|
||||
table: "SquareBarDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_RoundTubeDimensions",
|
||||
table: "RoundTubeDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_RoundBarDimensions",
|
||||
table: "RoundBarDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_RectangularTubeDimensions",
|
||||
table: "RectangularTubeDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_PipeDimensions",
|
||||
table: "PipeDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_MaterialDimensions",
|
||||
table: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_IBeamDimensions",
|
||||
table: "IBeamDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_FlatBarDimensions",
|
||||
table: "FlatBarDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_ChannelDimensions",
|
||||
table: "ChannelDimensions");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AngleDimensions",
|
||||
table: "AngleDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "SquareTubeDimensions",
|
||||
newName: "DimSquareTube");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "SquareBarDimensions",
|
||||
newName: "DimSquareBar");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "RoundTubeDimensions",
|
||||
newName: "DimRoundTube");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "RoundBarDimensions",
|
||||
newName: "DimRoundBar");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "RectangularTubeDimensions",
|
||||
newName: "DimRectangularTube");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "PipeDimensions",
|
||||
newName: "DimPipe");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "MaterialDimensions",
|
||||
newName: "DimBase");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "IBeamDimensions",
|
||||
newName: "DimIBeam");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "FlatBarDimensions",
|
||||
newName: "DimFlatBar");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "ChannelDimensions",
|
||||
newName: "DimChannel");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "AngleDimensions",
|
||||
newName: "DimAngle");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_SquareTubeDimensions_Size",
|
||||
table: "DimSquareTube",
|
||||
newName: "IX_DimSquareTube_Size");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_SquareBarDimensions_Size",
|
||||
table: "DimSquareBar",
|
||||
newName: "IX_DimSquareBar_Size");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_RoundTubeDimensions_OuterDiameter",
|
||||
table: "DimRoundTube",
|
||||
newName: "IX_DimRoundTube_OuterDiameter");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_RoundBarDimensions_Diameter",
|
||||
table: "DimRoundBar",
|
||||
newName: "IX_DimRoundBar_Diameter");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_RectangularTubeDimensions_Width",
|
||||
table: "DimRectangularTube",
|
||||
newName: "IX_DimRectangularTube_Width");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_PipeDimensions_NominalSize",
|
||||
table: "DimPipe",
|
||||
newName: "IX_DimPipe_NominalSize");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_MaterialDimensions_MaterialId",
|
||||
table: "DimBase",
|
||||
newName: "IX_DimBase_MaterialId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_IBeamDimensions_Height",
|
||||
table: "DimIBeam",
|
||||
newName: "IX_DimIBeam_Height");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_FlatBarDimensions_Width",
|
||||
table: "DimFlatBar",
|
||||
newName: "IX_DimFlatBar_Width");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_ChannelDimensions_Height",
|
||||
table: "DimChannel",
|
||||
newName: "IX_DimChannel_Height");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_AngleDimensions_Leg1",
|
||||
table: "DimAngle",
|
||||
newName: "IX_DimAngle_Leg1");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimSquareTube",
|
||||
table: "DimSquareTube",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimSquareBar",
|
||||
table: "DimSquareBar",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimRoundTube",
|
||||
table: "DimRoundTube",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimRoundBar",
|
||||
table: "DimRoundBar",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimRectangularTube",
|
||||
table: "DimRectangularTube",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimPipe",
|
||||
table: "DimPipe",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimBase",
|
||||
table: "DimBase",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimIBeam",
|
||||
table: "DimIBeam",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimFlatBar",
|
||||
table: "DimFlatBar",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimChannel",
|
||||
table: "DimChannel",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_DimAngle",
|
||||
table: "DimAngle",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimAngle_DimBase_Id",
|
||||
table: "DimAngle",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimBase_Materials_MaterialId",
|
||||
table: "DimBase",
|
||||
column: "MaterialId",
|
||||
principalTable: "Materials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimChannel_DimBase_Id",
|
||||
table: "DimChannel",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimFlatBar_DimBase_Id",
|
||||
table: "DimFlatBar",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimIBeam_DimBase_Id",
|
||||
table: "DimIBeam",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimPipe_DimBase_Id",
|
||||
table: "DimPipe",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimRectangularTube_DimBase_Id",
|
||||
table: "DimRectangularTube",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimRoundBar_DimBase_Id",
|
||||
table: "DimRoundBar",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimRoundTube_DimBase_Id",
|
||||
table: "DimRoundTube",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimSquareBar_DimBase_Id",
|
||||
table: "DimSquareBar",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DimSquareTube_DimBase_Id",
|
||||
table: "DimSquareTube",
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimAngle_DimBase_Id",
|
||||
table: "DimAngle");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimBase_Materials_MaterialId",
|
||||
table: "DimBase");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimChannel_DimBase_Id",
|
||||
table: "DimChannel");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimFlatBar_DimBase_Id",
|
||||
table: "DimFlatBar");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimIBeam_DimBase_Id",
|
||||
table: "DimIBeam");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimPipe_DimBase_Id",
|
||||
table: "DimPipe");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimRectangularTube_DimBase_Id",
|
||||
table: "DimRectangularTube");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimRoundBar_DimBase_Id",
|
||||
table: "DimRoundBar");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimRoundTube_DimBase_Id",
|
||||
table: "DimRoundTube");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimSquareBar_DimBase_Id",
|
||||
table: "DimSquareBar");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DimSquareTube_DimBase_Id",
|
||||
table: "DimSquareTube");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimSquareTube",
|
||||
table: "DimSquareTube");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimSquareBar",
|
||||
table: "DimSquareBar");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimRoundTube",
|
||||
table: "DimRoundTube");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimRoundBar",
|
||||
table: "DimRoundBar");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimRectangularTube",
|
||||
table: "DimRectangularTube");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimPipe",
|
||||
table: "DimPipe");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimIBeam",
|
||||
table: "DimIBeam");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimFlatBar",
|
||||
table: "DimFlatBar");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimChannel",
|
||||
table: "DimChannel");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimBase",
|
||||
table: "DimBase");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_DimAngle",
|
||||
table: "DimAngle");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimSquareTube",
|
||||
newName: "SquareTubeDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimSquareBar",
|
||||
newName: "SquareBarDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimRoundTube",
|
||||
newName: "RoundTubeDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimRoundBar",
|
||||
newName: "RoundBarDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimRectangularTube",
|
||||
newName: "RectangularTubeDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimPipe",
|
||||
newName: "PipeDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimIBeam",
|
||||
newName: "IBeamDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimFlatBar",
|
||||
newName: "FlatBarDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimChannel",
|
||||
newName: "ChannelDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimBase",
|
||||
newName: "MaterialDimensions");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "DimAngle",
|
||||
newName: "AngleDimensions");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimSquareTube_Size",
|
||||
table: "SquareTubeDimensions",
|
||||
newName: "IX_SquareTubeDimensions_Size");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimSquareBar_Size",
|
||||
table: "SquareBarDimensions",
|
||||
newName: "IX_SquareBarDimensions_Size");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimRoundTube_OuterDiameter",
|
||||
table: "RoundTubeDimensions",
|
||||
newName: "IX_RoundTubeDimensions_OuterDiameter");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimRoundBar_Diameter",
|
||||
table: "RoundBarDimensions",
|
||||
newName: "IX_RoundBarDimensions_Diameter");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimRectangularTube_Width",
|
||||
table: "RectangularTubeDimensions",
|
||||
newName: "IX_RectangularTubeDimensions_Width");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimPipe_NominalSize",
|
||||
table: "PipeDimensions",
|
||||
newName: "IX_PipeDimensions_NominalSize");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimIBeam_Height",
|
||||
table: "IBeamDimensions",
|
||||
newName: "IX_IBeamDimensions_Height");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimFlatBar_Width",
|
||||
table: "FlatBarDimensions",
|
||||
newName: "IX_FlatBarDimensions_Width");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimChannel_Height",
|
||||
table: "ChannelDimensions",
|
||||
newName: "IX_ChannelDimensions_Height");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimBase_MaterialId",
|
||||
table: "MaterialDimensions",
|
||||
newName: "IX_MaterialDimensions_MaterialId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DimAngle_Leg1",
|
||||
table: "AngleDimensions",
|
||||
newName: "IX_AngleDimensions_Leg1");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_SquareTubeDimensions",
|
||||
table: "SquareTubeDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_SquareBarDimensions",
|
||||
table: "SquareBarDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_RoundTubeDimensions",
|
||||
table: "RoundTubeDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_RoundBarDimensions",
|
||||
table: "RoundBarDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_RectangularTubeDimensions",
|
||||
table: "RectangularTubeDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_PipeDimensions",
|
||||
table: "PipeDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_IBeamDimensions",
|
||||
table: "IBeamDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_FlatBarDimensions",
|
||||
table: "FlatBarDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_ChannelDimensions",
|
||||
table: "ChannelDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_MaterialDimensions",
|
||||
table: "MaterialDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AngleDimensions",
|
||||
table: "AngleDimensions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AngleDimensions_MaterialDimensions_Id",
|
||||
table: "AngleDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ChannelDimensions_MaterialDimensions_Id",
|
||||
table: "ChannelDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
|
||||
table: "FlatBarDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_IBeamDimensions_MaterialDimensions_Id",
|
||||
table: "IBeamDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MaterialDimensions_Materials_MaterialId",
|
||||
table: "MaterialDimensions",
|
||||
column: "MaterialId",
|
||||
principalTable: "Materials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PipeDimensions_MaterialDimensions_Id",
|
||||
table: "PipeDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
|
||||
table: "RectangularTubeDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
|
||||
table: "RoundBarDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
|
||||
table: "RoundTubeDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
|
||||
table: "SquareBarDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
|
||||
table: "SquareTubeDimensions",
|
||||
column: "Id",
|
||||
principalTable: "MaterialDimensions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
// <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("20260216191345_DimensionsTPTtoTPC")]
|
||||
partial class DimensionsTPTtoTPC
|
||||
{
|
||||
/// <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.HasSequence("MaterialDimensionsSequence");
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<decimal>("KerfInches")
|
||||
.HasPrecision(6, 4)
|
||||
.HasColumnType("decimal(6,4)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CuttingTools");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
IsActive = true,
|
||||
IsDefault = true,
|
||||
KerfInches = 0.0625m,
|
||||
Name = "Bandsaw"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
IsActive = true,
|
||||
IsDefault = false,
|
||||
KerfInches = 0.125m,
|
||||
Name = "Chop Saw"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
IsActive = true,
|
||||
IsDefault = false,
|
||||
KerfInches = 0.0625m,
|
||||
Name = "Cold Cut Saw"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
IsActive = true,
|
||||
IsDefault = false,
|
||||
KerfInches = 0.0625m,
|
||||
Name = "Hacksaw"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
b.Property<string>("Customer")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("CuttingToolId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("JobNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("LockedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OptimizationResultJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("OptimizedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CuttingToolId");
|
||||
|
||||
b.HasIndex("JobNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.ToTable("JobParts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsCustomLength")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("JobId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("LengthInches")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("StockItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasIndex("MaterialId");
|
||||
|
||||
b.HasIndex("StockItemId");
|
||||
|
||||
b.ToTable("JobStocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("Grade")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Shape")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Size")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Materials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MaterialId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable((string)null);
|
||||
|
||||
b.UseTpcMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.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")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Leg1");
|
||||
|
||||
b.ToTable("DimAngle", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Flange")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Web")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("DimChannel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Thickness")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("DimFlatBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("WeightPerFoot")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.ToTable("DimIBeam", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("NominalSize")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<string>("Schedule")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<decimal?>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("NominalSize");
|
||||
|
||||
b.ToTable("DimPipe", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.ToTable("DimRectangularTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Diameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Diameter");
|
||||
|
||||
b.ToTable("DimRoundBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("OuterDiameter")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("OuterDiameter");
|
||||
|
||||
b.ToTable("DimRoundTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("DimSquareBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
{
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.ToTable("DimSquareTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("CuttingToolId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("CuttingTool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||
.WithMany("Parts")
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithMany("JobParts")
|
||||
.HasForeignKey("MaterialId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Job");
|
||||
|
||||
b.Navigation("Material");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||
.WithMany("Stock")
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithMany()
|
||||
.HasForeignKey("MaterialId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("StockItemId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Job");
|
||||
|
||||
b.Navigation("Material");
|
||||
|
||||
b.Navigation("StockItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
|
||||
{
|
||||
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||
.WithOne("Dimensions")
|
||||
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Material");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.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,172 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CutList.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DimensionsTPTtoTPC : Migration
|
||||
{
|
||||
private static readonly string[] DimTables =
|
||||
[
|
||||
"DimAngle", "DimChannel", "DimFlatBar", "DimIBeam", "DimPipe",
|
||||
"DimRectangularTube", "DimRoundBar", "DimRoundTube", "DimSquareBar", "DimSquareTube"
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1. Drop FKs from shape tables to DimBase
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: $"FK_{table}_DimBase_Id",
|
||||
table: table);
|
||||
}
|
||||
|
||||
// 2. Add MaterialId column to each shape table (nullable initially)
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaterialId",
|
||||
table: table,
|
||||
type: "int",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
// 3. Copy MaterialId from DimBase into each shape table
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
$"UPDATE t SET t.MaterialId = b.MaterialId FROM [{table}] t INNER JOIN [DimBase] b ON t.Id = b.Id");
|
||||
}
|
||||
|
||||
// 4. Make MaterialId non-nullable now that data is populated
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "MaterialId",
|
||||
table: table,
|
||||
type: "int",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
// 5. Drop DimBase
|
||||
migrationBuilder.DropTable(name: "DimBase");
|
||||
|
||||
// 6. Create shared sequence for unique IDs across all shape tables
|
||||
migrationBuilder.CreateSequence(name: "MaterialDimensionsSequence");
|
||||
|
||||
// 7. Switch Id columns to use the sequence
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Id",
|
||||
table: table,
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValueSql: "NEXT VALUE FOR [MaterialDimensionsSequence]",
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
}
|
||||
|
||||
// 8. Create indexes and FKs for MaterialId on each shape table
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: $"IX_{table}_MaterialId",
|
||||
table: table,
|
||||
column: "MaterialId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: $"FK_{table}_Materials_MaterialId",
|
||||
table: table,
|
||||
column: "MaterialId",
|
||||
principalTable: "Materials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Drop FKs and indexes from shape tables
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: $"FK_{table}_Materials_MaterialId",
|
||||
table: table);
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: $"IX_{table}_MaterialId",
|
||||
table: table);
|
||||
}
|
||||
|
||||
// Remove sequence from Id columns
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Id",
|
||||
table: table,
|
||||
type: "int",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldDefaultValueSql: "NEXT VALUE FOR [MaterialDimensionsSequence]");
|
||||
}
|
||||
|
||||
migrationBuilder.DropSequence(name: "MaterialDimensionsSequence");
|
||||
|
||||
// Re-create DimBase
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DimBase",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
MaterialId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DimBase", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DimBase_Materials_MaterialId",
|
||||
column: x => x.MaterialId,
|
||||
principalTable: "Materials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DimBase_MaterialId",
|
||||
table: "DimBase",
|
||||
column: "MaterialId",
|
||||
unique: true);
|
||||
|
||||
// Copy data back to DimBase from all shape tables
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
$"SET IDENTITY_INSERT [DimBase] ON; INSERT INTO [DimBase] (Id, MaterialId) SELECT Id, MaterialId FROM [{table}]; SET IDENTITY_INSERT [DimBase] OFF;");
|
||||
}
|
||||
|
||||
// Re-add FKs from shape tables to DimBase and drop MaterialId
|
||||
foreach (var table in DimTables)
|
||||
{
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: $"FK_{table}_DimBase_Id",
|
||||
table: table,
|
||||
column: "Id",
|
||||
principalTable: "DimBase",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.DropColumn(name: "MaterialId", table: table);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ namespace CutList.Web.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.HasSequence("MaterialDimensionsSequence");
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -119,6 +121,12 @@ namespace CutList.Web.Migrations
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("OptimizationResultJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("OptimizedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
@@ -268,14 +276,10 @@ namespace CutList.Web.Migrations
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DimensionType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("nvarchar(21)");
|
||||
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("MaterialId")
|
||||
.HasColumnType("int");
|
||||
@@ -285,11 +289,9 @@ namespace CutList.Web.Migrations
|
||||
b.HasIndex("MaterialId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MaterialDimensions");
|
||||
b.ToTable((string)null);
|
||||
|
||||
b.HasDiscriminator<string>("DimensionType").HasValue("MaterialDimensions");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
b.UseTpcMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
|
||||
@@ -521,14 +523,12 @@ namespace CutList.Web.Migrations
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Thickness")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Thickness");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Leg1");
|
||||
|
||||
b.HasDiscriminator().HasValue("Angle");
|
||||
b.ToTable("DimAngle", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
|
||||
@@ -540,10 +540,8 @@ namespace CutList.Web.Migrations
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Height");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Web")
|
||||
.HasPrecision(10, 4)
|
||||
@@ -551,7 +549,7 @@ namespace CutList.Web.Migrations
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.HasDiscriminator().HasValue("Channel");
|
||||
b.ToTable("DimChannel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
|
||||
@@ -559,20 +557,16 @@ namespace CutList.Web.Migrations
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Thickness")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Thickness");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Width");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.HasDiscriminator().HasValue("FlatBar");
|
||||
b.ToTable("DimFlatBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
|
||||
@@ -580,10 +574,8 @@ namespace CutList.Web.Migrations
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Height");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("WeightPerFoot")
|
||||
.HasPrecision(10, 4)
|
||||
@@ -591,7 +583,7 @@ namespace CutList.Web.Migrations
|
||||
|
||||
b.HasIndex("Height");
|
||||
|
||||
b.HasDiscriminator().HasValue("IBeam");
|
||||
b.ToTable("DimIBeam", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
|
||||
@@ -607,14 +599,12 @@ namespace CutList.Web.Migrations
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<decimal?>("Wall")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Wall");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("NominalSize");
|
||||
|
||||
b.HasDiscriminator().HasValue("Pipe");
|
||||
b.ToTable("DimPipe", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
|
||||
@@ -622,26 +612,20 @@ namespace CutList.Web.Migrations
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Height")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Height");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Wall");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Width")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Width");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Width");
|
||||
|
||||
b.HasDiscriminator().HasValue("RectangularTube");
|
||||
b.ToTable("DimRectangularTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
|
||||
@@ -654,7 +638,7 @@ namespace CutList.Web.Migrations
|
||||
|
||||
b.HasIndex("Diameter");
|
||||
|
||||
b.HasDiscriminator().HasValue("RoundBar");
|
||||
b.ToTable("DimRoundBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
|
||||
@@ -666,14 +650,12 @@ namespace CutList.Web.Migrations
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Wall");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("OuterDiameter");
|
||||
|
||||
b.HasDiscriminator().HasValue("RoundTube");
|
||||
b.ToTable("DimRoundTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
|
||||
@@ -681,14 +663,12 @@ namespace CutList.Web.Migrations
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Size");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.HasDiscriminator().HasValue("SquareBar");
|
||||
b.ToTable("DimSquareBar", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
|
||||
@@ -696,20 +676,16 @@ namespace CutList.Web.Migrations
|
||||
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Size");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<decimal>("Wall")
|
||||
.ValueGeneratedOnUpdateSometimes()
|
||||
.HasPrecision(10, 4)
|
||||
.HasColumnType("decimal(10,4)")
|
||||
.HasColumnName("Wall");
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.HasIndex("Size");
|
||||
|
||||
b.HasDiscriminator().HasValue("SquareTube");
|
||||
b.ToTable("DimSquareTube", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||
|
||||
+10
-1
@@ -10,6 +10,9 @@ builder.Services.AddControllers();
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Add Entity Framework
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
@@ -22,11 +25,17 @@ builder.Services.AddScoped<JobService>();
|
||||
builder.Services.AddScoped<CutListPackingService>();
|
||||
builder.Services.AddScoped<ReportService>();
|
||||
builder.Services.AddScoped<PurchaseItemService>();
|
||||
builder.Services.AddScoped<CatalogService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
app.UseHsts();
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class CatalogService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly MaterialService _materialService;
|
||||
|
||||
public CatalogService(ApplicationDbContext context, MaterialService materialService)
|
||||
{
|
||||
_context = context;
|
||||
_materialService = materialService;
|
||||
}
|
||||
|
||||
public async Task<CatalogData> ExportAsync()
|
||||
{
|
||||
var suppliers = await _context.Suppliers
|
||||
.Where(s => s.IsActive)
|
||||
.OrderBy(s => s.Name)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var cuttingTools = await _context.CuttingTools
|
||||
.Where(t => t.IsActive)
|
||||
.OrderBy(t => t.Name)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var materials = await _context.Materials
|
||||
.Include(m => m.Dimensions)
|
||||
.Include(m => m.StockItems.Where(s => s.IsActive))
|
||||
.ThenInclude(s => s.SupplierOfferings.Where(o => o.IsActive))
|
||||
.Where(m => m.IsActive)
|
||||
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var grouped = materials.GroupBy(m => m.Shape);
|
||||
var materialsDto = new CatalogMaterialsDto();
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
foreach (var m in group)
|
||||
{
|
||||
var stockItems = MapStockItems(m, suppliers);
|
||||
|
||||
switch (m.Shape)
|
||||
{
|
||||
case MaterialShape.Angle when m.Dimensions is AngleDimensions d:
|
||||
materialsDto.Angles.Add(new CatalogAngleDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Leg1 = d.Leg1, Leg2 = d.Leg2, Thickness = d.Thickness,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.Channel when m.Dimensions is ChannelDimensions d:
|
||||
materialsDto.Channels.Add(new CatalogChannelDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Height = d.Height, Flange = d.Flange, Web = d.Web,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.FlatBar when m.Dimensions is FlatBarDimensions d:
|
||||
materialsDto.FlatBars.Add(new CatalogFlatBarDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Width = d.Width, Thickness = d.Thickness,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.IBeam when m.Dimensions is IBeamDimensions d:
|
||||
materialsDto.IBeams.Add(new CatalogIBeamDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Height = d.Height, WeightPerFoot = d.WeightPerFoot,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.Pipe when m.Dimensions is PipeDimensions d:
|
||||
materialsDto.Pipes.Add(new CatalogPipeDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
NominalSize = d.NominalSize, Wall = d.Wall ?? 0, Schedule = d.Schedule,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.RectangularTube when m.Dimensions is RectangularTubeDimensions d:
|
||||
materialsDto.RectangularTubes.Add(new CatalogRectangularTubeDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Width = d.Width, Height = d.Height, Wall = d.Wall,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.RoundBar when m.Dimensions is RoundBarDimensions d:
|
||||
materialsDto.RoundBars.Add(new CatalogRoundBarDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
Diameter = d.Diameter,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.RoundTube when m.Dimensions is RoundTubeDimensions d:
|
||||
materialsDto.RoundTubes.Add(new CatalogRoundTubeDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
OuterDiameter = d.OuterDiameter, Wall = d.Wall,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.SquareBar when m.Dimensions is SquareBarDimensions d:
|
||||
materialsDto.SquareBars.Add(new CatalogSquareBarDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
SideLength = d.Size,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
case MaterialShape.SquareTube when m.Dimensions is SquareTubeDimensions d:
|
||||
materialsDto.SquareTubes.Add(new CatalogSquareTubeDto
|
||||
{
|
||||
Type = m.Type.ToString(), Grade = m.Grade, Size = m.Size, Description = m.Description,
|
||||
SideLength = d.Size, Wall = d.Wall,
|
||||
StockItems = stockItems
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CatalogData
|
||||
{
|
||||
ExportedAt = DateTime.UtcNow,
|
||||
Suppliers = suppliers.Select(s => new CatalogSupplierDto
|
||||
{
|
||||
Name = s.Name,
|
||||
ContactInfo = s.ContactInfo,
|
||||
Notes = s.Notes
|
||||
}).ToList(),
|
||||
CuttingTools = cuttingTools.Select(t => new CatalogCuttingToolDto
|
||||
{
|
||||
Name = t.Name,
|
||||
KerfInches = t.KerfInches,
|
||||
IsDefault = t.IsDefault
|
||||
}).ToList(),
|
||||
Materials = materialsDto
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ImportResultDto> ImportAsync(CatalogData data)
|
||||
{
|
||||
var result = new ImportResultDto();
|
||||
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Suppliers - upsert by name
|
||||
var supplierMap = await ImportSuppliersAsync(data.Suppliers, result);
|
||||
|
||||
// 2. Cutting tools - upsert by name
|
||||
await ImportCuttingToolsAsync(data.CuttingTools, result);
|
||||
|
||||
// 3. Materials + stock items + offerings
|
||||
await ImportAllMaterialsAsync(data.Materials, supplierMap, result);
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
result.Errors.Add($"Transaction failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, int>> ImportSuppliersAsync(
|
||||
List<CatalogSupplierDto> suppliers, ImportResultDto result)
|
||||
{
|
||||
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var existingSuppliers = await _context.Suppliers.ToListAsync();
|
||||
|
||||
foreach (var dto in suppliers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = existingSuppliers.FirstOrDefault(
|
||||
s => s.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.ContactInfo = dto.ContactInfo ?? existing.ContactInfo;
|
||||
existing.Notes = dto.Notes ?? existing.Notes;
|
||||
existing.IsActive = true;
|
||||
map[dto.Name] = existing.Id;
|
||||
result.SuppliersUpdated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var supplier = new Supplier
|
||||
{
|
||||
Name = dto.Name,
|
||||
ContactInfo = dto.ContactInfo,
|
||||
Notes = dto.Notes,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
_context.Suppliers.Add(supplier);
|
||||
await _context.SaveChangesAsync();
|
||||
existingSuppliers.Add(supplier);
|
||||
map[dto.Name] = supplier.Id;
|
||||
result.SuppliersCreated++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add($"Supplier '{dto.Name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return map;
|
||||
}
|
||||
|
||||
private async Task ImportCuttingToolsAsync(
|
||||
List<CatalogCuttingToolDto> tools, ImportResultDto result)
|
||||
{
|
||||
var existingTools = await _context.CuttingTools.ToListAsync();
|
||||
|
||||
foreach (var dto in tools)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = existingTools.FirstOrDefault(
|
||||
t => t.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.KerfInches = dto.KerfInches;
|
||||
existing.IsActive = true;
|
||||
result.CuttingToolsUpdated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var tool = new CuttingTool
|
||||
{
|
||||
Name = dto.Name,
|
||||
KerfInches = dto.KerfInches,
|
||||
IsDefault = false
|
||||
};
|
||||
_context.CuttingTools.Add(tool);
|
||||
existingTools.Add(tool);
|
||||
result.CuttingToolsCreated++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add($"Cutting tool '{dto.Name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ImportAllMaterialsAsync(
|
||||
CatalogMaterialsDto materials, Dictionary<string, int> supplierMap, ImportResultDto result)
|
||||
{
|
||||
var existingMaterials = await _context.Materials
|
||||
.Include(m => m.Dimensions)
|
||||
.Include(m => m.StockItems)
|
||||
.ThenInclude(s => s.SupplierOfferings)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var dto in materials.Angles)
|
||||
await ImportMaterialAsync(dto, MaterialShape.Angle, existingMaterials, supplierMap, result,
|
||||
() => new AngleDimensions { Leg1 = dto.Leg1, Leg2 = dto.Leg2, Thickness = dto.Thickness },
|
||||
dim => { var d = (AngleDimensions)dim; d.Leg1 = dto.Leg1; d.Leg2 = dto.Leg2; d.Thickness = dto.Thickness; });
|
||||
|
||||
foreach (var dto in materials.Channels)
|
||||
await ImportMaterialAsync(dto, MaterialShape.Channel, existingMaterials, supplierMap, result,
|
||||
() => new ChannelDimensions { Height = dto.Height, Flange = dto.Flange, Web = dto.Web },
|
||||
dim => { var d = (ChannelDimensions)dim; d.Height = dto.Height; d.Flange = dto.Flange; d.Web = dto.Web; });
|
||||
|
||||
foreach (var dto in materials.FlatBars)
|
||||
await ImportMaterialAsync(dto, MaterialShape.FlatBar, existingMaterials, supplierMap, result,
|
||||
() => new FlatBarDimensions { Width = dto.Width, Thickness = dto.Thickness },
|
||||
dim => { var d = (FlatBarDimensions)dim; d.Width = dto.Width; d.Thickness = dto.Thickness; });
|
||||
|
||||
foreach (var dto in materials.IBeams)
|
||||
await ImportMaterialAsync(dto, MaterialShape.IBeam, existingMaterials, supplierMap, result,
|
||||
() => new IBeamDimensions { Height = dto.Height, WeightPerFoot = dto.WeightPerFoot },
|
||||
dim => { var d = (IBeamDimensions)dim; d.Height = dto.Height; d.WeightPerFoot = dto.WeightPerFoot; });
|
||||
|
||||
foreach (var dto in materials.Pipes)
|
||||
await ImportMaterialAsync(dto, MaterialShape.Pipe, existingMaterials, supplierMap, result,
|
||||
() => new PipeDimensions { NominalSize = dto.NominalSize, Wall = dto.Wall, Schedule = dto.Schedule },
|
||||
dim => { var d = (PipeDimensions)dim; d.NominalSize = dto.NominalSize; d.Wall = (decimal?)dto.Wall; d.Schedule = dto.Schedule; });
|
||||
|
||||
foreach (var dto in materials.RectangularTubes)
|
||||
await ImportMaterialAsync(dto, MaterialShape.RectangularTube, existingMaterials, supplierMap, result,
|
||||
() => new RectangularTubeDimensions { Width = dto.Width, Height = dto.Height, Wall = dto.Wall },
|
||||
dim => { var d = (RectangularTubeDimensions)dim; d.Width = dto.Width; d.Height = dto.Height; d.Wall = dto.Wall; });
|
||||
|
||||
foreach (var dto in materials.RoundBars)
|
||||
await ImportMaterialAsync(dto, MaterialShape.RoundBar, existingMaterials, supplierMap, result,
|
||||
() => new RoundBarDimensions { Diameter = dto.Diameter },
|
||||
dim => { var d = (RoundBarDimensions)dim; d.Diameter = dto.Diameter; });
|
||||
|
||||
foreach (var dto in materials.RoundTubes)
|
||||
await ImportMaterialAsync(dto, MaterialShape.RoundTube, existingMaterials, supplierMap, result,
|
||||
() => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter, Wall = dto.Wall },
|
||||
dim => { var d = (RoundTubeDimensions)dim; d.OuterDiameter = dto.OuterDiameter; d.Wall = dto.Wall; });
|
||||
|
||||
foreach (var dto in materials.SquareBars)
|
||||
await ImportMaterialAsync(dto, MaterialShape.SquareBar, existingMaterials, supplierMap, result,
|
||||
() => new SquareBarDimensions { Size = dto.SideLength },
|
||||
dim => { var d = (SquareBarDimensions)dim; d.Size = dto.SideLength; });
|
||||
|
||||
foreach (var dto in materials.SquareTubes)
|
||||
await ImportMaterialAsync(dto, MaterialShape.SquareTube, existingMaterials, supplierMap, result,
|
||||
() => new SquareTubeDimensions { Size = dto.SideLength, Wall = dto.Wall },
|
||||
dim => { var d = (SquareTubeDimensions)dim; d.Size = dto.SideLength; d.Wall = dto.Wall; });
|
||||
}
|
||||
|
||||
private async Task ImportMaterialAsync(
|
||||
CatalogMaterialBaseDto dto, MaterialShape shape,
|
||||
List<Material> existingMaterials, Dictionary<string, int> supplierMap,
|
||||
ImportResultDto result,
|
||||
Func<MaterialDimensions> createDimensions,
|
||||
Action<MaterialDimensions> updateDimensions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, ignoreCase: true, out var type))
|
||||
{
|
||||
type = MaterialType.Steel;
|
||||
result.Warnings.Add($"Material '{shape} - {dto.Size}': Unknown type '{dto.Type}', defaulting to Steel");
|
||||
}
|
||||
|
||||
var existing = existingMaterials.FirstOrDefault(
|
||||
m => m.Shape == shape && m.Size.Equals(dto.Size, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Material material;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Type = type;
|
||||
existing.Grade = dto.Grade ?? existing.Grade;
|
||||
existing.Description = dto.Description ?? existing.Description;
|
||||
existing.IsActive = true;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
if (existing.Dimensions != null)
|
||||
{
|
||||
updateDimensions(existing.Dimensions);
|
||||
existing.SortOrder = existing.Dimensions.GetSortOrder();
|
||||
}
|
||||
|
||||
material = existing;
|
||||
result.MaterialsUpdated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
material = new Material
|
||||
{
|
||||
Shape = shape,
|
||||
Type = type,
|
||||
Grade = dto.Grade,
|
||||
Size = dto.Size,
|
||||
Description = dto.Description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var dimensions = createDimensions();
|
||||
material = await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
existingMaterials.Add(material);
|
||||
result.MaterialsCreated++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await ImportStockItemsAsync(material, dto.StockItems, supplierMap, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add($"Material '{shape} - {dto.Size}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImportStockItemsAsync(
|
||||
Material material, List<CatalogStockItemDto> stockItems,
|
||||
Dictionary<string, int> supplierMap, ImportResultDto result)
|
||||
{
|
||||
var existingStockItems = await _context.StockItems
|
||||
.Include(s => s.SupplierOfferings)
|
||||
.Where(s => s.MaterialId == material.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var dto in stockItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = existingStockItems.FirstOrDefault(
|
||||
s => s.LengthInches == dto.LengthInches);
|
||||
|
||||
StockItem stockItem;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Name = dto.Name ?? existing.Name;
|
||||
existing.Notes = dto.Notes ?? existing.Notes;
|
||||
existing.IsActive = true;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
stockItem = existing;
|
||||
result.StockItemsUpdated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
stockItem = new StockItem
|
||||
{
|
||||
MaterialId = material.Id,
|
||||
LengthInches = dto.LengthInches,
|
||||
Name = dto.Name,
|
||||
QuantityOnHand = dto.QuantityOnHand,
|
||||
Notes = dto.Notes,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
_context.StockItems.Add(stockItem);
|
||||
await _context.SaveChangesAsync();
|
||||
existingStockItems.Add(stockItem);
|
||||
result.StockItemsCreated++;
|
||||
}
|
||||
|
||||
foreach (var offeringDto in dto.SupplierOfferings)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!supplierMap.TryGetValue(offeringDto.SupplierName, out var supplierId))
|
||||
{
|
||||
result.Warnings.Add(
|
||||
$"Offering for stock '{material.DisplayName} @ {dto.LengthInches}\"': " +
|
||||
$"Unknown supplier '{offeringDto.SupplierName}', skipped");
|
||||
continue;
|
||||
}
|
||||
|
||||
var existingOffering = stockItem.SupplierOfferings.FirstOrDefault(
|
||||
o => o.SupplierId == supplierId);
|
||||
|
||||
if (existingOffering != null)
|
||||
{
|
||||
existingOffering.PartNumber = offeringDto.PartNumber ?? existingOffering.PartNumber;
|
||||
existingOffering.SupplierDescription = offeringDto.SupplierDescription ?? existingOffering.SupplierDescription;
|
||||
existingOffering.Price = offeringDto.Price ?? existingOffering.Price;
|
||||
existingOffering.Notes = offeringDto.Notes ?? existingOffering.Notes;
|
||||
existingOffering.IsActive = true;
|
||||
result.OfferingsUpdated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var offering = new SupplierOffering
|
||||
{
|
||||
StockItemId = stockItem.Id,
|
||||
SupplierId = supplierId,
|
||||
PartNumber = offeringDto.PartNumber,
|
||||
SupplierDescription = offeringDto.SupplierDescription,
|
||||
Price = offeringDto.Price,
|
||||
Notes = offeringDto.Notes
|
||||
};
|
||||
_context.SupplierOfferings.Add(offering);
|
||||
stockItem.SupplierOfferings.Add(offering);
|
||||
result.OfferingsCreated++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add(
|
||||
$"Offering for '{material.DisplayName} @ {dto.LengthInches}\"' " +
|
||||
$"from '{offeringDto.SupplierName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add(
|
||||
$"Stock item '{material.DisplayName} @ {dto.LengthInches}\"': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CatalogStockItemDto> MapStockItems(Material m, List<Supplier> suppliers)
|
||||
{
|
||||
return m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
|
||||
{
|
||||
LengthInches = s.LengthInches,
|
||||
Name = s.Name,
|
||||
QuantityOnHand = s.QuantityOnHand,
|
||||
Notes = s.Notes,
|
||||
SupplierOfferings = s.SupplierOfferings.Select(o => new CatalogSupplierOfferingDto
|
||||
{
|
||||
SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown",
|
||||
PartNumber = o.PartNumber,
|
||||
SupplierDescription = o.SupplierDescription,
|
||||
Price = o.Price,
|
||||
Notes = o.Notes
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,18 @@ public class CutListPackingService
|
||||
return result;
|
||||
}
|
||||
|
||||
public MultiMaterialPackResult? LoadSavedResult(string json)
|
||||
{
|
||||
var saved = System.Text.Json.JsonSerializer.Deserialize<SavedOptimizationResult>(json);
|
||||
return saved?.ToPackResult(_context);
|
||||
}
|
||||
|
||||
public string SerializeResult(MultiMaterialPackResult result)
|
||||
{
|
||||
var saved = SavedOptimizationResult.FromPackResult(result);
|
||||
return System.Text.Json.JsonSerializer.Serialize(saved);
|
||||
}
|
||||
|
||||
public MultiMaterialPackingSummary GetSummary(MultiMaterialPackResult result)
|
||||
{
|
||||
var summary = new MultiMaterialPackingSummary();
|
||||
@@ -275,3 +287,109 @@ public class MaterialPackingSummary
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
|
||||
// --- Serialization DTOs for persisting optimization results ---
|
||||
|
||||
public class SavedBinItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double Length { get; set; }
|
||||
}
|
||||
|
||||
public class SavedBin
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public double Spacing { get; set; }
|
||||
public List<SavedBinItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SavedMaterialResult
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialDisplayName { get; set; } = string.Empty;
|
||||
public List<SavedBin> InStockBins { get; set; } = new();
|
||||
public List<SavedBin> ToBePurchasedBins { get; set; } = new();
|
||||
public List<SavedBinItem> ItemsNotPlaced { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SavedOptimizationResult
|
||||
{
|
||||
public DateTime OptimizedAt { get; set; }
|
||||
public List<SavedMaterialResult> MaterialResults { get; set; } = new();
|
||||
|
||||
public static SavedOptimizationResult FromPackResult(MultiMaterialPackResult result)
|
||||
{
|
||||
var saved = new SavedOptimizationResult
|
||||
{
|
||||
OptimizedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
foreach (var mr in result.MaterialResults)
|
||||
{
|
||||
var savedMr = new SavedMaterialResult
|
||||
{
|
||||
MaterialId = mr.Material.Id,
|
||||
MaterialDisplayName = mr.Material.DisplayName
|
||||
};
|
||||
|
||||
savedMr.InStockBins = mr.InStockBins.Select(ToBinDto).ToList();
|
||||
savedMr.ToBePurchasedBins = mr.ToBePurchasedBins.Select(ToBinDto).ToList();
|
||||
savedMr.ItemsNotPlaced = mr.PackResult.ItemsNotUsed
|
||||
.Select(i => new SavedBinItem { Name = i.Name, Length = i.Length })
|
||||
.ToList();
|
||||
|
||||
saved.MaterialResults.Add(savedMr);
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public MultiMaterialPackResult ToPackResult(ApplicationDbContext context)
|
||||
{
|
||||
var result = new MultiMaterialPackResult();
|
||||
|
||||
foreach (var savedMr in MaterialResults)
|
||||
{
|
||||
var material = context.Materials.Find(savedMr.MaterialId);
|
||||
if (material == null) continue;
|
||||
|
||||
var packResult = new PackResult();
|
||||
var inStockBins = savedMr.InStockBins.Select(FromBinDto).ToList();
|
||||
var toBePurchasedBins = savedMr.ToBePurchasedBins.Select(FromBinDto).ToList();
|
||||
|
||||
// Add all bins to PackResult so summary calculations work
|
||||
foreach (var bin in inStockBins) packResult.AddBin(bin);
|
||||
foreach (var bin in toBePurchasedBins) packResult.AddBin(bin);
|
||||
foreach (var item in savedMr.ItemsNotPlaced)
|
||||
packResult.AddItemNotUsed(new BinItem(item.Name, item.Length));
|
||||
|
||||
result.MaterialResults.Add(new MaterialPackResult
|
||||
{
|
||||
Material = material,
|
||||
PackResult = packResult,
|
||||
InStockBins = inStockBins,
|
||||
ToBePurchasedBins = toBePurchasedBins
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SavedBin ToBinDto(Bin bin)
|
||||
{
|
||||
return new SavedBin
|
||||
{
|
||||
Length = bin.Length,
|
||||
Spacing = bin.Spacing,
|
||||
Items = bin.Items.Select(i => new SavedBinItem { Name = i.Name, Length = i.Length }).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static Bin FromBinDto(SavedBin dto)
|
||||
{
|
||||
var bin = new Bin(dto.Length) { Spacing = dto.Spacing };
|
||||
foreach (var item in dto.Items)
|
||||
bin.AddItem(new BinItem(item.Name, item.Length));
|
||||
return bin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ public class JobService
|
||||
public async Task UpdateAsync(Job job)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
_context.Jobs.Update(job);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
@@ -161,6 +163,29 @@ public class JobService
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
// Optimization result persistence
|
||||
public async Task SaveOptimizationResultAsync(int jobId, string resultJson, DateTime optimizedAt)
|
||||
{
|
||||
var job = await _context.Jobs.FindAsync(jobId);
|
||||
if (job != null)
|
||||
{
|
||||
job.OptimizationResultJson = resultJson;
|
||||
job.OptimizedAt = optimizedAt;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearOptimizationResultAsync(int jobId)
|
||||
{
|
||||
var job = await _context.Jobs.FindAsync(jobId);
|
||||
if (job != null && job.OptimizationResultJson != null)
|
||||
{
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Parts management
|
||||
public async Task<JobPart> AddPartAsync(JobPart part)
|
||||
{
|
||||
@@ -172,11 +197,13 @@ public class JobService
|
||||
_context.JobParts.Add(part);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Update job timestamp
|
||||
// Update job timestamp and clear stale results
|
||||
var job = await _context.Jobs.FindAsync(part.JobId);
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -192,6 +219,8 @@ public class JobService
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -209,6 +238,8 @@ public class JobService
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -229,6 +260,8 @@ public class JobService
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -244,6 +277,8 @@ public class JobService
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -261,6 +296,8 @@ public class JobService
|
||||
if (job != null)
|
||||
{
|
||||
job.UpdatedAt = DateTime.UtcNow;
|
||||
job.OptimizationResultJson = null;
|
||||
job.OptimizedAt = null;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,11 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Cut list material headers — hidden on screen, shown in print via repeating thead */
|
||||
.cutlist-material-print-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Print styles - Compact layout to save paper */
|
||||
@media print {
|
||||
body {
|
||||
@@ -299,18 +304,23 @@
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide redundant stock summary (shown per-material) */
|
||||
.print-stock-summary {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* General card print styles */
|
||||
.card {
|
||||
border: 1px solid #ccc !important;
|
||||
/* Keep purchase list with cut lists to save paper */
|
||||
.print-purchase-list {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/* General card print styles — allow large cards to break across pages */
|
||||
.card {
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
/* Keep card headers with the start of their content */
|
||||
.card-header {
|
||||
break-after: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
@@ -319,6 +329,42 @@
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
/* Cut list tables: hide screen header, show repeating print header in thead */
|
||||
.cutlist-material-screen-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cutlist-material-print-header {
|
||||
display: table-row !important;
|
||||
}
|
||||
|
||||
.cutlist-material-print-header th {
|
||||
background: #f0f0f0 !important;
|
||||
padding: 0.4rem 0.5rem !important;
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.cutlist-material-name {
|
||||
font-size: 12pt;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cutlist-material-stats {
|
||||
float: right;
|
||||
font-size: 9pt;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Remove card border/padding for cut list cards in print — table handles it */
|
||||
.cutlist-material-card {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.cutlist-material-card > .card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Reduce spacing */
|
||||
.mb-4 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Alro Steel SmartGrid Scraper — Remaining Steps
|
||||
|
||||
## Status: Script is READY TO RUN
|
||||
|
||||
The scraper at `scripts/AlroCatalog/scrape_alro.py` is complete and tested. Discovery mode confirmed it works correctly against the live site.
|
||||
|
||||
## What's Done
|
||||
1. Script written with correct ASP.NET control IDs (discovered via `--discover` mode)
|
||||
2. Level 1 (main grid) navigation: working
|
||||
3. Level 2 (popup grid) navigation: working
|
||||
4. Level 3 (dims panel) scraping: working — uses cascading dropdowns `ddlDimA` → `ddlDimB` → `ddlDimC` → `ddlLength`
|
||||
5. Grade filter: 11 common grades (A-36, 1018, 1045, 1144, 12L14, etc.)
|
||||
6. Size string normalization: "1-1/2\"" matches O'Neal format
|
||||
7. Progress save/resume: working
|
||||
8. Discovery mode verified: A-36 Round bars → 27 sizes, 80 items (lengths include "20 FT", "Custom Cut List", "Drop/Remnant" — non-stock entries filtered out in catalog builder)
|
||||
|
||||
## Remaining Steps
|
||||
|
||||
### Step 1: Run the full scrape
|
||||
```bash
|
||||
cd C:\Users\aisaacs\Desktop\Projects\CutList
|
||||
python scripts/AlroCatalog/scrape_alro.py
|
||||
```
|
||||
- This scrapes all 3 categories (Bars, Pipe/Tube, Structural) for 11 filtered grades
|
||||
- Takes ~30-60 minutes (cascading dropdown selections with 1.5s delay each)
|
||||
- Progress saved incrementally to `scripts/AlroCatalog/alro-scrape-progress.json`
|
||||
- If interrupted, resume with `python scripts/AlroCatalog/scrape_alro.py --resume`
|
||||
- To scrape ALL grades: `python scripts/AlroCatalog/scrape_alro.py --all-grades`
|
||||
|
||||
### Step 2: Review output
|
||||
- Output: `CutList.Web/Data/SeedData/alro-catalog.json`
|
||||
- Verify material counts, shapes, sizes
|
||||
- Spot-check dimensions against myalro.com
|
||||
- Compare shape coverage to O'Neal catalog
|
||||
|
||||
### Step 3: Post-scrape adjustments (if needed)
|
||||
|
||||
**Dimension mapping for Structural/Pipe shapes**: The `build_size_and_dims()` function handles all shapes but Structural (Angle, Channel, Beam) and Pipe/Tube shapes haven't been tested live yet. After scraping, check the screenshots in `scripts/AlroCatalog/screenshots/` to verify dimension mapping. The first item of each new shape gets a screenshot + HTML dump.
|
||||
|
||||
**Known dimension mapping assumptions:**
|
||||
- Angle: DimA = leg size, DimB = thickness → `"leg1 x leg2 x thickness"` (assumes equal legs)
|
||||
- Channel: DimA = height, DimB = flange → needs verification
|
||||
- IBeam: DimA = depth, DimB = weight/ft → `"W{depth} x {wt}"`
|
||||
- SquareTube: DimA = size, DimB = wall
|
||||
- RectTube: DimA = width, DimB = height, DimC = wall
|
||||
- RoundTube: DimA = OD, DimB = wall
|
||||
- Pipe: DimA = NPS, DimB = schedule
|
||||
|
||||
**If dimension mapping is wrong for a shape**: Edit the `build_size_and_dims()` function in `scrape_alro.py` and re-run just the catalog builder:
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from scripts.AlroCatalog.scrape_alro import build_catalog
|
||||
data = json.load(open('scripts/AlroCatalog/alro-scrape-progress.json'))
|
||||
catalog = build_catalog(data['items'])
|
||||
json.dump(catalog, open('CutList.Web/Data/SeedData/alro-catalog.json', 'w'), indent=2)
|
||||
"
|
||||
```
|
||||
|
||||
### Step 4: Part numbers (optional future enhancement)
|
||||
The current scraper captures sizes and lengths but NOT part numbers. To get part numbers, the script would need to:
|
||||
1. Select DimA + DimB + Length
|
||||
2. Click the "Next >" button (`btnSearch`)
|
||||
3. Capture part number from the results panel
|
||||
4. Click Back
|
||||
|
||||
This adds significant time per item. The catalog works without part numbers — the supplierOfferings have empty partNumber/supplierDescription fields.
|
||||
|
||||
## Key Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `scripts/AlroCatalog/scrape_alro.py` | The scraper script |
|
||||
| `scripts/AlroCatalog/alro-scrape-progress.json` | Incremental progress (resume support) |
|
||||
| `scripts/AlroCatalog/screenshots/` | Discovery HTML/screenshots per shape |
|
||||
| `CutList.Web/Data/SeedData/alro-catalog.json` | Final output (same schema as oneals-catalog.json) |
|
||||
| `CutList.Web/Data/SeedData/oneals-catalog.json` | Reference format |
|
||||
|
||||
## Grade Filter (editable in script)
|
||||
Located at line ~50 in `scrape_alro.py`. Current filter:
|
||||
- A-36, 1018 CF, 1018 HR, 1044 HR, 1045 CF, 1045 HR, 1045 TG&P
|
||||
- 1144 CF, 1144 HR, 12L14 CF, A311/Stressproof
|
||||
|
||||
To add/remove grades, edit the `GRADE_FILTER` set in the script.
|
||||
@@ -0,0 +1,976 @@
|
||||
{
|
||||
"completed": [
|
||||
[
|
||||
"Bars",
|
||||
"A-36",
|
||||
"ROUND"
|
||||
],
|
||||
[
|
||||
"Bars",
|
||||
"A-36",
|
||||
"FLAT"
|
||||
]
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".188",
|
||||
"dim_a_text": "3/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".188",
|
||||
"dim_a_text": "3/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".250",
|
||||
"dim_a_text": "1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".250",
|
||||
"dim_a_text": "1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".250",
|
||||
"dim_a_text": "1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".313",
|
||||
"dim_a_text": "5/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".313",
|
||||
"dim_a_text": "5/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".313",
|
||||
"dim_a_text": "5/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".375",
|
||||
"dim_a_text": "3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".375",
|
||||
"dim_a_text": "3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".375",
|
||||
"dim_a_text": "3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".438",
|
||||
"dim_a_text": "7/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".438",
|
||||
"dim_a_text": "7/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".438",
|
||||
"dim_a_text": "7/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".500",
|
||||
"dim_a_text": "1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".500",
|
||||
"dim_a_text": "1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".500",
|
||||
"dim_a_text": "1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".563",
|
||||
"dim_a_text": "9/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".563",
|
||||
"dim_a_text": "9/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".563",
|
||||
"dim_a_text": "9/16",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".625",
|
||||
"dim_a_text": "5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".625",
|
||||
"dim_a_text": "5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".625",
|
||||
"dim_a_text": "5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".750",
|
||||
"dim_a_text": "3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".750",
|
||||
"dim_a_text": "3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".750",
|
||||
"dim_a_text": "3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".875",
|
||||
"dim_a_text": "7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".875",
|
||||
"dim_a_text": "7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": ".875",
|
||||
"dim_a_text": "7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.000",
|
||||
"dim_a_text": "1",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.000",
|
||||
"dim_a_text": "1",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.000",
|
||||
"dim_a_text": "1",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.125",
|
||||
"dim_a_text": "1 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.125",
|
||||
"dim_a_text": "1 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.125",
|
||||
"dim_a_text": "1 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.250",
|
||||
"dim_a_text": "1 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.250",
|
||||
"dim_a_text": "1 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.250",
|
||||
"dim_a_text": "1 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.375",
|
||||
"dim_a_text": "1 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.375",
|
||||
"dim_a_text": "1 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.375",
|
||||
"dim_a_text": "1 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.500",
|
||||
"dim_a_text": "1 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.500",
|
||||
"dim_a_text": "1 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.500",
|
||||
"dim_a_text": "1 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.625",
|
||||
"dim_a_text": "1 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.625",
|
||||
"dim_a_text": "1 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.625",
|
||||
"dim_a_text": "1 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.750",
|
||||
"dim_a_text": "1 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.750",
|
||||
"dim_a_text": "1 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.750",
|
||||
"dim_a_text": "1 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.875",
|
||||
"dim_a_text": "1 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.875",
|
||||
"dim_a_text": "1 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "1.875",
|
||||
"dim_a_text": "1 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.000",
|
||||
"dim_a_text": "2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.000",
|
||||
"dim_a_text": "2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.000",
|
||||
"dim_a_text": "2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.125",
|
||||
"dim_a_text": "2 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.125",
|
||||
"dim_a_text": "2 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.125",
|
||||
"dim_a_text": "2 1/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.250",
|
||||
"dim_a_text": "2 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.250",
|
||||
"dim_a_text": "2 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.250",
|
||||
"dim_a_text": "2 1/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.375",
|
||||
"dim_a_text": "2 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.375",
|
||||
"dim_a_text": "2 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.375",
|
||||
"dim_a_text": "2 3/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.500",
|
||||
"dim_a_text": "2 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.500",
|
||||
"dim_a_text": "2 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.500",
|
||||
"dim_a_text": "2 1/2",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.625",
|
||||
"dim_a_text": "2 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.625",
|
||||
"dim_a_text": "2 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.625",
|
||||
"dim_a_text": "2 5/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.750",
|
||||
"dim_a_text": "2 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.750",
|
||||
"dim_a_text": "2 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.750",
|
||||
"dim_a_text": "2 3/4",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.875",
|
||||
"dim_a_text": "2 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.875",
|
||||
"dim_a_text": "2 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "2.875",
|
||||
"dim_a_text": "2 7/8",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "3.000",
|
||||
"dim_a_text": "3",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Custom Cut List",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "3.000",
|
||||
"dim_a_text": "3",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "Drop/Remnant",
|
||||
"length_inches": null
|
||||
},
|
||||
{
|
||||
"grade": "A-36",
|
||||
"shape": "RoundBar",
|
||||
"dim_a_val": "3.000",
|
||||
"dim_a_text": "3",
|
||||
"dim_b_val": null,
|
||||
"dim_b_text": null,
|
||||
"dim_c_val": null,
|
||||
"dim_c_text": null,
|
||||
"length_text": "20 FT",
|
||||
"length_inches": 240.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,790 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Alro Steel SmartGrid Scraper
|
||||
Scrapes myalro.com's SmartGrid for Carbon Steel materials and outputs
|
||||
a catalog JSON matching the O'Neal catalog format.
|
||||
|
||||
Usage:
|
||||
python scrape_alro.py # Scrape filtered grades (resumes from saved progress)
|
||||
python scrape_alro.py --all-grades # Scrape ALL grades (slow)
|
||||
python scrape_alro.py --discover # Scrape first item only, dump HTML/screenshots
|
||||
python scrape_alro.py --fresh # Start fresh, ignoring saved progress
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from playwright.async_api import async_playwright, Page, TimeoutError as PwTimeout
|
||||
from playwright_stealth import Stealth
|
||||
|
||||
# ── Logging ──────────────────────────────────────────────────────────
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# ── Paths ────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
OUTPUT_PATH = (SCRIPT_DIR / "../../CutList.Web/Data/SeedData/alro-catalog.json").resolve()
|
||||
PROGRESS_PATH = SCRIPT_DIR / "alro-scrape-progress.json"
|
||||
SCREENSHOTS_DIR = SCRIPT_DIR / "screenshots"
|
||||
|
||||
# ── Config ───────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://www.myalro.com/SmartGrid.aspx?PT=Steel&Clear=true"
|
||||
DELAY = 5 # seconds between postback clicks
|
||||
TIMEOUT = 15_000 # ms for element waits
|
||||
CS_ROW = 4 # Carbon Steel row index in main grid
|
||||
|
||||
CATEGORIES = ["Bars", "Pipe / Tube", "Structural"]
|
||||
|
||||
# ┌─────────────────────────────────────────────────────────────────┐
|
||||
# │ GRADE FILTER — only these grades will be scraped. │
|
||||
# │ Use --all-grades flag to override and scrape everything. │
|
||||
# │ Grade names must match the gpname attribute exactly. │
|
||||
# └─────────────────────────────────────────────────────────────────┘
|
||||
GRADE_FILTER = {
|
||||
# Common structural / general purpose
|
||||
"A-36",
|
||||
# Mild steel
|
||||
"1018 CF",
|
||||
"1018 HR",
|
||||
# Medium carbon (shafts, gears, pins)
|
||||
"1045 CF",
|
||||
"1045 HR",
|
||||
"1045 TG&P",
|
||||
# Free-machining
|
||||
"1144 CF",
|
||||
"1144 HR",
|
||||
"12L14 CF",
|
||||
# Hot-rolled plate/bar
|
||||
"1044 HR",
|
||||
# Stressproof (high-strength shafting)
|
||||
"A311/Stressproof",
|
||||
}
|
||||
|
||||
# Alro shape column header → our MaterialShape enum
|
||||
SHAPE_MAP = {
|
||||
"ROUND": "RoundBar",
|
||||
"FLAT": "FlatBar",
|
||||
"SQUARE": "SquareBar",
|
||||
"ANGLE": "Angle",
|
||||
"CHANNEL": "Channel",
|
||||
"BEAM": "IBeam",
|
||||
"SQ TUBE": "SquareTube",
|
||||
"SQUARE TUBE": "SquareTube",
|
||||
"REC TUBE": "RectangularTube",
|
||||
"RECT TUBE": "RectangularTube",
|
||||
"RECTANGULAR TUBE": "RectangularTube",
|
||||
"ROUND TUBE": "RoundTube",
|
||||
"RND TUBE": "RoundTube",
|
||||
"PIPE": "Pipe",
|
||||
}
|
||||
|
||||
# ── ASP.NET control IDs ─────────────────────────────────────────
|
||||
_CP = "ctl00_ContentPlaceHolder1"
|
||||
_PU = f"{_CP}_pnlPopUP"
|
||||
ID = dict(
|
||||
main_grid = f"{_CP}_grdMain",
|
||||
popup_grid = f"{_PU}_grdPopUp",
|
||||
popup_window = f"{_PU}_Window",
|
||||
dims_panel = f"{_PU}_upnlDims",
|
||||
back_btn = f"{_PU}_btnBack",
|
||||
# Dimension dropdowns (cascading: A → B → C → Length)
|
||||
dim_a = f"{_PU}_ddlDimA",
|
||||
dim_b = f"{_PU}_ddlDimB",
|
||||
dim_c = f"{_PU}_ddlDimC",
|
||||
dim_length = f"{_PU}_ddlLength",
|
||||
btn_next = f"{_PU}_btnSearch",
|
||||
)
|
||||
|
||||
# Postback targets ($ separators)
|
||||
PB = dict(
|
||||
main_grid = "ctl00$ContentPlaceHolder1$grdMain",
|
||||
popup_grid = "ctl00$ContentPlaceHolder1$pnlPopUP$grdPopUp",
|
||||
back_btn = "ctl00$ContentPlaceHolder1$pnlPopUP$btnBack",
|
||||
popup = "ctl00$ContentPlaceHolder1$pnlPopUP",
|
||||
dim_a = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimA",
|
||||
dim_b = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimB",
|
||||
dim_c = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimC",
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Utility helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def parse_fraction(s: str) -> float | None:
|
||||
"""Parse fraction/decimal string → float. '1-1/4' → 1.25, '.250' → 0.25"""
|
||||
if not s:
|
||||
return None
|
||||
s = s.strip().strip('"\'')
|
||||
# Collapse double spaces from Alro dropdown text ("1 1/4" → "1 1/4")
|
||||
s = re.sub(r"\s+", " ", s)
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
pass
|
||||
# Mixed fraction: "1-1/4" or "1 1/4"
|
||||
m = re.match(r"^(\d+)[\s-](\d+)/(\d+)$", s)
|
||||
if m:
|
||||
return int(m[1]) + int(m[2]) / int(m[3])
|
||||
m = re.match(r"^(\d+)/(\d+)$", s)
|
||||
if m:
|
||||
return int(m[1]) / int(m[2])
|
||||
m = re.match(r"^(\d+)$", s)
|
||||
if m:
|
||||
return float(m[1])
|
||||
return None
|
||||
|
||||
|
||||
def decimal_to_fraction(value: float) -> str:
|
||||
"""0.25 → '1/4', 1.25 → '1-1/4', 3.0 → '3'"""
|
||||
if value <= 0:
|
||||
return "0"
|
||||
whole = int(value)
|
||||
frac = value - whole
|
||||
if abs(frac) < 0.001:
|
||||
return str(whole)
|
||||
from math import gcd
|
||||
sixteenths = round(frac * 16)
|
||||
if sixteenths == 16:
|
||||
return str(whole + 1)
|
||||
g = gcd(sixteenths, 16)
|
||||
num, den = sixteenths // g, 16 // g
|
||||
frac_s = f"{num}/{den}"
|
||||
return f"{whole}-{frac_s}" if whole else frac_s
|
||||
|
||||
|
||||
def normalize_dim_text(s: str) -> str:
|
||||
"""Normalize dimension text: '1 1/4' → '1-1/4', '3/16' → '3/16'"""
|
||||
s = re.sub(r"\s+", " ", s.strip())
|
||||
# "1 1/4" → "1-1/4" (mixed fraction with space → hyphen)
|
||||
s = re.sub(r"^(\d+)\s+(\d+/\d+)$", r"\1-\2", s)
|
||||
return s
|
||||
|
||||
|
||||
def parse_length_to_inches(text: str) -> float | None:
|
||||
"""Parse length string to inches. \"20'\" → 240, \"240\" → 240"""
|
||||
s = text.strip().upper()
|
||||
s = re.sub(r"\s*(RL|RANDOM.*|LENGTHS?|EA|EACH|STOCK)\s*", "", s).strip()
|
||||
m = re.match(r"^(\d+(?:\.\d+)?)\s*['\u2032]", s)
|
||||
if m:
|
||||
return float(m[1]) * 12
|
||||
m = re.match(r"^(\d+(?:\.\d+)?)\s*FT", s)
|
||||
if m:
|
||||
return float(m[1]) * 12
|
||||
m = re.match(r'^(\d+(?:\.\d+)?)\s*"?\s*$', s)
|
||||
if m:
|
||||
v = float(m[1])
|
||||
return v * 12 if v <= 30 else v
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# SmartGrid navigation
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
async def wait_for_update(page: Page, timeout: int = TIMEOUT):
|
||||
"""Wait for ASP.NET partial postback to finish."""
|
||||
try:
|
||||
await page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
except PwTimeout:
|
||||
log.warning(" networkidle timeout – continuing")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
async def do_postback(page: Page, target: str, arg: str):
|
||||
"""Execute a __doPostBack call."""
|
||||
await page.evaluate(f"__doPostBack('{target}', '{arg}')")
|
||||
|
||||
|
||||
async def click_category(page: Page, category: str) -> bool:
|
||||
"""Click a category blue-button for Carbon Steel in the main grid."""
|
||||
log.info(f"Clicking main grid: {category} (row {CS_ROW})")
|
||||
arg = f"{category}${CS_ROW}"
|
||||
link = await page.query_selector(
|
||||
f"#{ID['main_grid']} a[href*=\"'{arg}'\"] img[src*='blue_button']"
|
||||
)
|
||||
if not link:
|
||||
log.error(f" Button not found for {arg}")
|
||||
return False
|
||||
|
||||
parent = await link.evaluate_handle("el => el.parentElement")
|
||||
await parent.as_element().click()
|
||||
|
||||
try:
|
||||
await page.wait_for_selector(f"#{ID['popup_grid']}", state="visible", timeout=TIMEOUT)
|
||||
await wait_for_update(page)
|
||||
return True
|
||||
except PwTimeout:
|
||||
log.error(f" Popup did not appear for {category}")
|
||||
return False
|
||||
|
||||
|
||||
async def scrape_popup_grid(page: Page):
|
||||
"""Parse the popup grid → [(grade_name, grade_id, shape, row_idx, has_btn)]."""
|
||||
headers = await page.eval_on_selector_all(
|
||||
f"#{ID['popup_grid']} tr.DataHeader th",
|
||||
"els => els.map(el => el.textContent.trim())",
|
||||
)
|
||||
log.info(f" Popup columns: {headers}")
|
||||
|
||||
rows = await page.query_selector_all(
|
||||
f"#{ID['popup_grid']} tr.griditemP, #{ID['popup_grid']} tr.gridaltItemP"
|
||||
)
|
||||
combos = []
|
||||
for row_idx, row in enumerate(rows):
|
||||
first_td = await row.query_selector("td[gpid]")
|
||||
if not first_td:
|
||||
continue
|
||||
gid = (await first_td.get_attribute("gpid") or "").strip()
|
||||
gname = (await first_td.get_attribute("gpname") or "").strip()
|
||||
tds = await row.query_selector_all("td")
|
||||
for col_idx, td in enumerate(tds):
|
||||
if col_idx == 0:
|
||||
continue
|
||||
shape = headers[col_idx] if col_idx < len(headers) else ""
|
||||
img = await td.query_selector("img[src*='blue_button']")
|
||||
combos.append((gname, gid, shape, row_idx, img is not None))
|
||||
|
||||
active = sum(1 for c in combos if c[4])
|
||||
log.info(f" {active} active grade/shape combos")
|
||||
return combos
|
||||
|
||||
|
||||
async def click_shape(page: Page, shape: str, row_idx: int) -> bool:
|
||||
"""Click a shape button in the popup grid; wait for dims panel."""
|
||||
arg = f"{shape}${row_idx}"
|
||||
link = await page.query_selector(
|
||||
f"#{ID['popup_grid']} a[href*=\"'{arg}'\"] img[src*='blue_button']"
|
||||
)
|
||||
if not link:
|
||||
try:
|
||||
await do_postback(page, PB["popup_grid"], arg)
|
||||
except Exception:
|
||||
log.warning(f" Could not click shape {arg}")
|
||||
return False
|
||||
else:
|
||||
parent = await link.evaluate_handle("el => el.parentElement")
|
||||
await parent.as_element().click()
|
||||
|
||||
try:
|
||||
# Wait for the DimA dropdown to appear (the real indicator of dims panel loaded)
|
||||
await page.wait_for_selector(f"#{ID['dim_a']}", state="attached", timeout=TIMEOUT)
|
||||
await wait_for_update(page)
|
||||
return True
|
||||
except PwTimeout:
|
||||
# Check if panel has any content at all
|
||||
html = await page.inner_html(f"#{ID['dims_panel']}")
|
||||
if len(html.strip()) > 50:
|
||||
await wait_for_update(page)
|
||||
return True
|
||||
log.warning(f" Dims panel timeout for {arg}")
|
||||
return False
|
||||
|
||||
|
||||
async def click_back(page: Page):
|
||||
"""Click Back to return to the popup grid view."""
|
||||
try:
|
||||
await do_postback(page, PB["back_btn"], "")
|
||||
await wait_for_update(page)
|
||||
await asyncio.sleep(DELAY)
|
||||
except Exception as e:
|
||||
log.warning(f" Back button error: {e}")
|
||||
|
||||
|
||||
async def close_popup(page: Page):
|
||||
"""Close the popup window and return to the main grid."""
|
||||
try:
|
||||
await do_postback(page, PB["popup"], "Close")
|
||||
await wait_for_update(page)
|
||||
await asyncio.sleep(DELAY)
|
||||
except Exception as e:
|
||||
log.warning(f" Close popup error: {e}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Level 3 — Dimension Panel Scraping
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
async def get_select_options(page: Page, sel_id: str):
|
||||
"""Return [(value, text), ...] for a <select>, excluding placeholders."""
|
||||
el = await page.query_selector(f"#{sel_id}")
|
||||
if not el:
|
||||
return []
|
||||
# Check if disabled
|
||||
disabled = await el.get_attribute("disabled")
|
||||
if disabled:
|
||||
return []
|
||||
try:
|
||||
opts = await page.eval_on_selector(
|
||||
f"#{sel_id}",
|
||||
"""el => Array.from(el.options).map(o => ({
|
||||
v: o.value, t: o.text.trim(), d: o.disabled
|
||||
}))""",
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
return [
|
||||
(o["v"], o["t"])
|
||||
for o in opts
|
||||
if o["v"] and o["v"] != "-1" and o["t"] and not o["d"]
|
||||
and o["t"].lower() not in ("- select -", "--select--", "select...", "select", "")
|
||||
]
|
||||
|
||||
|
||||
async def scrape_dims_panel(page: Page, grade: str, shape_alro: str,
|
||||
shape_mapped: str, *, save_discovery: bool = False,
|
||||
on_item=None, scraped_dim_a: set[str] | None = None):
|
||||
"""Main Level 3 extraction. Returns list of raw item dicts.
|
||||
|
||||
If on_item callback is provided, it is called with each item dict
|
||||
as soon as it is discovered (for incremental saving).
|
||||
If scraped_dim_a is provided, DimA values in that set are skipped (resume).
|
||||
"""
|
||||
items: list[dict] = []
|
||||
|
||||
if save_discovery:
|
||||
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||||
safe = f"{grade}_{shape_alro}".replace(" ", "_").replace("/", "-")
|
||||
await page.screenshot(path=str(SCREENSHOTS_DIR / f"dims_{safe}.png"), full_page=True)
|
||||
html = await page.inner_html(f"#{ID['dims_panel']}")
|
||||
(SCREENSHOTS_DIR / f"dims_{safe}.html").write_text(html, encoding="utf-8")
|
||||
log.info(f" Discovery saved → screenshots/dims_{safe}.*")
|
||||
|
||||
# ── Get DimA options (primary dimension: diameter, width, size, etc.) ──
|
||||
dim_a_opts = await get_select_options(page, ID["dim_a"])
|
||||
if not dim_a_opts:
|
||||
log.warning(f" No DimA options found")
|
||||
try:
|
||||
html = await page.inner_html(f"#{ID['dims_panel']}")
|
||||
if len(html) > 50:
|
||||
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||||
safe = f"{grade}_{shape_alro}_nodimopts".replace(" ", "_").replace("/", "-")
|
||||
(SCREENSHOTS_DIR / f"{safe}.html").write_text(html, encoding="utf-8")
|
||||
except Exception as e:
|
||||
log.warning(f" Could not dump dims panel: {e}")
|
||||
return []
|
||||
|
||||
already_done = scraped_dim_a or set()
|
||||
remaining = [(v, t) for v, t in dim_a_opts if v not in already_done]
|
||||
if already_done:
|
||||
log.info(f" DimA: {len(dim_a_opts)} sizes ({len(dim_a_opts) - len(remaining)} already scraped, {len(remaining)} remaining)")
|
||||
else:
|
||||
log.info(f" DimA: {len(dim_a_opts)} sizes")
|
||||
|
||||
# All DimA values already scraped — combo is complete
|
||||
if not remaining:
|
||||
return []
|
||||
|
||||
for a_val, a_text in remaining:
|
||||
# Select DimA → triggers postback → DimB/Length populate
|
||||
await page.select_option(f"#{ID['dim_a']}", a_val)
|
||||
await asyncio.sleep(DELAY)
|
||||
await wait_for_update(page)
|
||||
|
||||
# Check if DimB appeared (secondary dimension: thickness, wall, etc.)
|
||||
dim_b_opts = await get_select_options(page, ID["dim_b"])
|
||||
if dim_b_opts:
|
||||
for b_val, b_text in dim_b_opts:
|
||||
await page.select_option(f"#{ID['dim_b']}", b_val)
|
||||
await asyncio.sleep(DELAY)
|
||||
await wait_for_update(page)
|
||||
|
||||
# Check for DimC (tertiary — rare)
|
||||
dim_c_opts = await get_select_options(page, ID["dim_c"])
|
||||
if dim_c_opts:
|
||||
for c_val, c_text in dim_c_opts:
|
||||
await page.select_option(f"#{ID['dim_c']}", c_val)
|
||||
await asyncio.sleep(DELAY)
|
||||
await wait_for_update(page)
|
||||
|
||||
lengths = await get_select_options(page, ID["dim_length"])
|
||||
for l_val, l_text in lengths:
|
||||
item = _make_item(
|
||||
grade, shape_mapped,
|
||||
a_val, a_text, b_val, b_text, c_val, c_text,
|
||||
l_text,
|
||||
)
|
||||
items.append(item)
|
||||
if on_item:
|
||||
on_item(item)
|
||||
else:
|
||||
# No DimC — read lengths
|
||||
lengths = await get_select_options(page, ID["dim_length"])
|
||||
for l_val, l_text in lengths:
|
||||
item = _make_item(
|
||||
grade, shape_mapped,
|
||||
a_val, a_text, b_val, b_text, None, None,
|
||||
l_text,
|
||||
)
|
||||
items.append(item)
|
||||
if on_item:
|
||||
on_item(item)
|
||||
else:
|
||||
# No DimB — just DimA + Length
|
||||
lengths = await get_select_options(page, ID["dim_length"])
|
||||
for l_val, l_text in lengths:
|
||||
item = _make_item(
|
||||
grade, shape_mapped,
|
||||
a_val, a_text, None, None, None, None,
|
||||
l_text,
|
||||
)
|
||||
items.append(item)
|
||||
if on_item:
|
||||
on_item(item)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def _make_item(grade, shape, a_val, a_text, b_val, b_text, c_val, c_text, l_text):
|
||||
"""Build a raw item dict from dimension selections."""
|
||||
return {
|
||||
"grade": grade,
|
||||
"shape": shape,
|
||||
"dim_a_val": a_val, # decimal string like ".500"
|
||||
"dim_a_text": a_text, # fraction string like "1/2"
|
||||
"dim_b_val": b_val,
|
||||
"dim_b_text": b_text,
|
||||
"dim_c_val": c_val,
|
||||
"dim_c_text": c_text,
|
||||
"length_text": l_text,
|
||||
"length_inches": parse_length_to_inches(l_text),
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Output — build catalog JSON
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_size_and_dims(shape: str, item: dict):
|
||||
"""Return (size_string, dimensions_dict) for a catalog material entry.
|
||||
|
||||
Uses the decimal values from dropdown option values for precision,
|
||||
and fraction text from dropdown option text for display.
|
||||
"""
|
||||
# Use the numeric value from the dropdown (e.g. ".500") for precision
|
||||
a = float(item["dim_a_val"]) if item.get("dim_a_val") else None
|
||||
b = float(item["dim_b_val"]) if item.get("dim_b_val") else None
|
||||
c = float(item["dim_c_val"]) if item.get("dim_c_val") else None
|
||||
|
||||
a_txt = normalize_dim_text(item.get("dim_a_text") or "")
|
||||
b_txt = normalize_dim_text(item.get("dim_b_text") or "")
|
||||
c_txt = normalize_dim_text(item.get("dim_c_text") or "")
|
||||
|
||||
if shape == "RoundBar" and a is not None:
|
||||
return f'{a_txt}"', {"diameter": round(a, 4)}
|
||||
|
||||
if shape == "FlatBar":
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" x {b_txt}"',
|
||||
{"width": round(a, 4), "thickness": round(b, 4)})
|
||||
if a is not None:
|
||||
return f'{a_txt}"', {"width": round(a, 4), "thickness": 0}
|
||||
|
||||
if shape == "SquareBar" and a is not None:
|
||||
return f'{a_txt}"', {"sideLength": round(a, 4)}
|
||||
|
||||
if shape == "Angle":
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" x {a_txt}" x {b_txt}"',
|
||||
{"leg1": round(a, 4), "leg2": round(a, 4), "thickness": round(b, 4)})
|
||||
if a is not None:
|
||||
return f'{a_txt}"', {"leg1": round(a, 4), "leg2": round(a, 4), "thickness": 0}
|
||||
|
||||
if shape == "Channel":
|
||||
# Channels may use DimA for combined designation or height
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" x {b_txt}"',
|
||||
{"height": round(a, 4), "flange": round(b, 4), "web": 0})
|
||||
if a is not None:
|
||||
return a_txt, {"height": round(a, 4), "flange": 0, "web": 0}
|
||||
|
||||
if shape == "IBeam":
|
||||
# DimA might be the W-designation, DimB the weight/ft
|
||||
if a is not None and b is not None:
|
||||
return (f"W{int(a)} x {b}",
|
||||
{"height": round(a, 4), "weightPerFoot": round(b, 4)})
|
||||
if a is not None:
|
||||
return f"W{int(a)}", {"height": round(a, 4), "weightPerFoot": 0}
|
||||
|
||||
if shape == "SquareTube":
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" x {b_txt}" wall',
|
||||
{"sideLength": round(a, 4), "wall": round(b, 4)})
|
||||
if a is not None:
|
||||
return f'{a_txt}"', {"sideLength": round(a, 4), "wall": 0}
|
||||
|
||||
if shape == "RectangularTube":
|
||||
if a is not None and b is not None and c is not None:
|
||||
return (f'{a_txt}" x {b_txt}" x {c_txt}" wall',
|
||||
{"width": round(a, 4), "height": round(b, 4), "wall": round(c, 4)})
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" x {b_txt}"',
|
||||
{"width": round(a, 4), "height": round(b, 4), "wall": 0})
|
||||
|
||||
if shape == "RoundTube":
|
||||
if a is not None and b is not None:
|
||||
return (f'{a_txt}" OD x {b_txt}" wall',
|
||||
{"outerDiameter": round(a, 4), "wall": round(b, 4)})
|
||||
if a is not None:
|
||||
return f'{a_txt}" OD', {"outerDiameter": round(a, 4), "wall": 0}
|
||||
|
||||
if shape == "Pipe":
|
||||
sched = b_txt or c_txt or "40"
|
||||
if a is not None:
|
||||
return (f'{a_txt}" NPS Sch {sched}',
|
||||
{"nominalSize": round(a, 4), "schedule": sched})
|
||||
|
||||
# Fallback
|
||||
return a_txt or "", {}
|
||||
|
||||
|
||||
SHAPE_GROUP_KEY = {
|
||||
"Angle": "angles",
|
||||
"Channel": "channels",
|
||||
"FlatBar": "flatBars",
|
||||
"IBeam": "iBeams",
|
||||
"Pipe": "pipes",
|
||||
"RectangularTube": "rectangularTubes",
|
||||
"RoundBar": "roundBars",
|
||||
"RoundTube": "roundTubes",
|
||||
"SquareBar": "squareBars",
|
||||
"SquareTube": "squareTubes",
|
||||
}
|
||||
|
||||
|
||||
def build_catalog(scraped: list[dict]) -> dict:
|
||||
"""Assemble the final catalog JSON from scraped item dicts."""
|
||||
materials: dict[tuple, dict] = {}
|
||||
|
||||
for item in scraped:
|
||||
shape = item.get("shape", "")
|
||||
grade = item.get("grade", "")
|
||||
if not shape or not grade:
|
||||
continue
|
||||
|
||||
size_str, dims = build_size_and_dims(shape, item)
|
||||
key = (shape, grade, size_str)
|
||||
|
||||
if key not in materials:
|
||||
mat = {
|
||||
"type": "Steel",
|
||||
"grade": grade,
|
||||
"size": size_str,
|
||||
"stockItems": [],
|
||||
}
|
||||
mat.update(dims)
|
||||
materials[key] = mat
|
||||
|
||||
length = item.get("length_inches")
|
||||
if length and length > 0:
|
||||
existing = {si["lengthInches"] for si in materials[key]["stockItems"]}
|
||||
if round(length, 4) not in existing:
|
||||
materials[key]["stockItems"].append({
|
||||
"lengthInches": round(length, 4),
|
||||
"quantityOnHand": 0,
|
||||
"supplierOfferings": [{
|
||||
"supplierName": "Alro Steel",
|
||||
"partNumber": "",
|
||||
"supplierDescription": "",
|
||||
}],
|
||||
})
|
||||
|
||||
# Group by shape key
|
||||
grouped: dict[str, list] = {v: [] for v in SHAPE_GROUP_KEY.values()}
|
||||
for (shape, _, _), mat in sorted(materials.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[0][2])):
|
||||
group_key = SHAPE_GROUP_KEY.get(shape)
|
||||
if group_key:
|
||||
grouped[group_key].append(mat)
|
||||
|
||||
return {
|
||||
"exportedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"suppliers": [{"name": "Alro Steel"}],
|
||||
"cuttingTools": [
|
||||
{"name": "Bandsaw", "kerfInches": 0.0625, "isDefault": True},
|
||||
{"name": "Chop Saw", "kerfInches": 0.125, "isDefault": False},
|
||||
{"name": "Cold Cut Saw", "kerfInches": 0.0625, "isDefault": False},
|
||||
{"name": "Hacksaw", "kerfInches": 0.0625, "isDefault": False},
|
||||
],
|
||||
"materials": grouped,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Progress management
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def load_progress() -> dict:
|
||||
if PROGRESS_PATH.exists():
|
||||
return json.loads(PROGRESS_PATH.read_text(encoding="utf-8"))
|
||||
return {"completed": [], "items": []}
|
||||
|
||||
|
||||
def save_progress(progress: dict):
|
||||
PROGRESS_PATH.write_text(json.dumps(progress, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Main
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
async def main():
|
||||
discover = "--discover" in sys.argv
|
||||
fresh = "--fresh" in sys.argv
|
||||
all_grades = "--all-grades" in sys.argv
|
||||
|
||||
progress = {"completed": [], "items": []} if fresh else load_progress()
|
||||
all_items: list[dict] = progress.get("items", [])
|
||||
done_keys: set[tuple] = {tuple(k) for k in progress.get("completed", [])}
|
||||
|
||||
# Build index of saved DimA values per (grade, shape) for partial resume
|
||||
saved_dim_a: dict[tuple[str, str], set[str]] = {}
|
||||
if all_items and not fresh:
|
||||
for item in all_items:
|
||||
key = (item.get("grade", ""), item.get("shape", ""))
|
||||
saved_dim_a.setdefault(key, set()).add(item.get("dim_a_val", ""))
|
||||
|
||||
log.info("Alro Steel SmartGrid Scraper")
|
||||
if all_grades:
|
||||
log.info(" Mode: ALL grades")
|
||||
else:
|
||||
log.info(f" Filtering to {len(GRADE_FILTER)} grades: {', '.join(sorted(GRADE_FILTER))}")
|
||||
if fresh:
|
||||
log.info(" Fresh start — ignoring saved progress")
|
||||
elif done_keys:
|
||||
log.info(f" Resuming: {len(done_keys)} combos done, {len(all_items)} items saved")
|
||||
if discover:
|
||||
log.info(" Discovery mode — will scrape first item then stop")
|
||||
|
||||
async with Stealth().use_async(async_playwright()) as pw:
|
||||
browser = await pw.chromium.launch(headless=False)
|
||||
ctx = await browser.new_context(
|
||||
viewport={"width": 1280, "height": 900},
|
||||
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
locale="en-US",
|
||||
timezone_id="America/Indiana/Indianapolis",
|
||||
)
|
||||
page = await ctx.new_page()
|
||||
|
||||
log.info(f"Navigating to SmartGrid …")
|
||||
await page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if not await page.query_selector(f"#{ID['main_grid']}"):
|
||||
log.error("Main grid not found! Saving screenshot.")
|
||||
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||||
await page.screenshot(path=str(SCREENSHOTS_DIR / "error_no_grid.png"))
|
||||
await browser.close()
|
||||
return
|
||||
|
||||
log.info("Main grid loaded")
|
||||
total_scraped = 0
|
||||
first_item = True
|
||||
|
||||
for category in CATEGORIES:
|
||||
log.info(f"\n{'=' * 60}")
|
||||
log.info(f" Category: {category}")
|
||||
log.info(f"{'=' * 60}")
|
||||
|
||||
if not await click_category(page, category):
|
||||
continue
|
||||
await asyncio.sleep(DELAY)
|
||||
|
||||
combos = await scrape_popup_grid(page)
|
||||
|
||||
for grade_name, grade_id, shape_name, row_idx, has_btn in combos:
|
||||
if not has_btn:
|
||||
continue
|
||||
|
||||
# Grade filter
|
||||
if not all_grades and grade_name not in GRADE_FILTER:
|
||||
continue
|
||||
|
||||
shape_upper = shape_name.upper().strip()
|
||||
shape_mapped = SHAPE_MAP.get(shape_upper)
|
||||
if shape_mapped is None:
|
||||
log.info(f" Skip unmapped shape: {shape_name}")
|
||||
continue
|
||||
|
||||
combo_key = (category, grade_name, shape_name)
|
||||
if combo_key in done_keys:
|
||||
log.info(f" Skip (done): {grade_name} / {shape_name}")
|
||||
continue
|
||||
|
||||
log.info(f"\n -- {grade_name} / {shape_name} -> {shape_mapped} --")
|
||||
|
||||
if not await click_shape(page, shape_name, row_idx):
|
||||
await click_back(page)
|
||||
await asyncio.sleep(DELAY)
|
||||
continue
|
||||
|
||||
await asyncio.sleep(DELAY)
|
||||
|
||||
combo_count = 0
|
||||
def on_item_discovered(item):
|
||||
nonlocal total_scraped, combo_count
|
||||
all_items.append(item)
|
||||
total_scraped += 1
|
||||
combo_count += 1
|
||||
progress["items"] = all_items
|
||||
save_progress(progress)
|
||||
|
||||
# Pass already-scraped DimA values so partial combos resume correctly
|
||||
already = saved_dim_a.get((grade_name, shape_mapped), set())
|
||||
|
||||
items = await scrape_dims_panel(
|
||||
page, grade_name, shape_name, shape_mapped,
|
||||
save_discovery=first_item or discover,
|
||||
on_item=on_item_discovered,
|
||||
scraped_dim_a=already,
|
||||
)
|
||||
first_item = False
|
||||
|
||||
log.info(f" -> {combo_count} items (total {total_scraped})")
|
||||
|
||||
done_keys.add(combo_key)
|
||||
progress["completed"] = [list(k) for k in done_keys]
|
||||
save_progress(progress)
|
||||
|
||||
await click_back(page)
|
||||
await asyncio.sleep(DELAY)
|
||||
|
||||
if discover:
|
||||
log.info("\nDiscovery done. Check: scripts/AlroCatalog/screenshots/")
|
||||
await browser.close()
|
||||
return
|
||||
|
||||
await close_popup(page)
|
||||
await asyncio.sleep(DELAY)
|
||||
|
||||
await browser.close()
|
||||
|
||||
# ── Build output ──
|
||||
log.info(f"\n{'=' * 60}")
|
||||
log.info(f"Building catalog from {len(all_items)} items …")
|
||||
catalog = build_catalog(all_items)
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT_PATH.write_text(json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
log.info(f"Written: {OUTPUT_PATH}")
|
||||
total_mats = sum(len(v) for v in catalog["materials"].values())
|
||||
total_stock = sum(len(m["stockItems"]) for v in catalog["materials"].values() for m in v)
|
||||
log.info(f"Materials: {total_mats}")
|
||||
log.info(f"Stock items: {total_stock}")
|
||||
for shape_key, mats in sorted(catalog["materials"].items()):
|
||||
if mats:
|
||||
log.info(f" {shape_key}: {len(mats)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,147 @@
|
||||
<#
|
||||
Deploy CutList.Web as a Windows Service
|
||||
|
||||
Examples:
|
||||
# Run from repository root:
|
||||
powershell -ExecutionPolicy Bypass -File scripts/Deploy-CutListWeb.ps1 -ServiceName CutListWeb -InstallDir C:\Services\CutListWeb -Urls "http://*:5270" -OpenFirewall
|
||||
|
||||
# Run from scripts directory:
|
||||
powershell -ExecutionPolicy Bypass -File Deploy-CutListWeb.ps1 -ServiceName CutListWeb -InstallDir C:\Services\CutListWeb -Urls "http://*:5270" -OpenFirewall
|
||||
|
||||
Requires: dotnet SDK/runtime installed and administrative privileges.
|
||||
#>
|
||||
|
||||
Param(
|
||||
[string]$ServiceName = "CutListWeb",
|
||||
[string]$PublishConfiguration = "Release",
|
||||
[string]$InstallDir = "C:\Services\CutListWeb",
|
||||
[string]$Urls = "http://*:5270",
|
||||
[switch]$OpenFirewall,
|
||||
[int]$ServiceStopTimeoutSeconds = 30,
|
||||
[int]$ServiceStartTimeoutSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Detect repository root (parent of scripts directory or current directory if already at root)
|
||||
$ScriptDir = Split-Path -Parent $PSCommandPath
|
||||
$RepoRoot = if ((Split-Path -Leaf $ScriptDir) -eq 'scripts') {
|
||||
Split-Path -Parent $ScriptDir
|
||||
} else {
|
||||
$ScriptDir
|
||||
}
|
||||
|
||||
Write-Host "Repository root: $RepoRoot"
|
||||
$ProjectPath = Join-Path $RepoRoot 'CutList.Web\CutList.Web.csproj'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ProjectPath)) {
|
||||
throw "Project not found at: $ProjectPath"
|
||||
}
|
||||
|
||||
function Ensure-Dir($path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
New-Item -ItemType Directory -Path $path | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-And-DeleteService($name) {
|
||||
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||
if ($null -ne $svc) {
|
||||
if ($svc.Status -ne 'Stopped') {
|
||||
Write-Host "Stopping service '$name'..."
|
||||
Stop-Service -Name $name -Force -ErrorAction SilentlyContinue
|
||||
try { $svc.WaitForStatus('Stopped',[TimeSpan]::FromSeconds($ServiceStopTimeoutSeconds)) | Out-Null } catch {}
|
||||
# If still running, kill by PID
|
||||
$q = & sc.exe queryex $name 2>$null
|
||||
$pidLine = $q | Where-Object { $_ -match 'PID' }
|
||||
if ($pidLine -and ($pidLine -match '(\d+)$')) {
|
||||
$procId = [int]$Matches[1]
|
||||
if ($procId -gt 0) {
|
||||
try { Write-Host "Killing service process PID=$procId ..."; Stop-Process -Id $procId -Force } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host "Deleting service '$name'..."
|
||||
sc.exe delete $name | Out-Null
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
|
||||
function Publish-App() {
|
||||
Write-Host "Publishing CutList.Web to $InstallDir ..."
|
||||
Ensure-Dir $InstallDir
|
||||
|
||||
# Run dotnet publish directly - output will be visible
|
||||
& dotnet publish $ProjectPath -c $PublishConfiguration -o $InstallDir
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet publish failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-ExeLocks($path) {
|
||||
$procs = Get-Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -and ($_.Path -ieq $path)
|
||||
}
|
||||
foreach ($p in $procs) {
|
||||
try { Write-Host "Killing process $($p.Id) $($p.ProcessName) ..."; Stop-Process -Id $p.Id -Force } catch {}
|
||||
}
|
||||
# Wait until unlocked
|
||||
for ($i=0; $i -lt 50; $i++) {
|
||||
$still = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Path -and ($_.Path -ieq $path) }
|
||||
if (-not $still) { break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
|
||||
function Create-Service($name, $bin, $urls) {
|
||||
$binPath = '"' + $bin + '" --urls ' + $urls
|
||||
Write-Host "Creating service '$name' with binPath: $binPath"
|
||||
# Note: space after '=' is required for sc.exe syntax
|
||||
sc.exe create $name binPath= "$binPath" start= auto DisplayName= "$name" | Out-Null
|
||||
# Set recovery to restart on failure
|
||||
sc.exe failure $name reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||
sc.exe description $name 'CutList bin packing web application' | Out-Null
|
||||
}
|
||||
|
||||
function Start-ServiceSafe($name) {
|
||||
Write-Host "Starting service '$name'..."
|
||||
Start-Service -Name $name
|
||||
(Get-Service -Name $name).WaitForStatus('Running',[TimeSpan]::FromSeconds($ServiceStartTimeoutSeconds)) | Out-Null
|
||||
sc.exe query $name | Write-Host
|
||||
}
|
||||
|
||||
if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) {
|
||||
throw "dotnet SDK/Runtime not found in PATH. Please install .NET 8+ or add it to PATH."
|
||||
}
|
||||
|
||||
Stop-And-DeleteService -name $ServiceName
|
||||
Stop-ExeLocks -path (Join-Path $InstallDir 'CutList.Web.exe')
|
||||
try { Remove-Item -LiteralPath (Join-Path $InstallDir 'CutList.Web.exe') -Force -ErrorAction SilentlyContinue } catch {}
|
||||
Publish-App
|
||||
|
||||
$exe = Join-Path $InstallDir 'CutList.Web.exe'
|
||||
if (-not (Test-Path -LiteralPath $exe)) {
|
||||
throw "Expected published executable not found: $exe"
|
||||
}
|
||||
|
||||
Create-Service -name $ServiceName -bin $exe -urls $Urls
|
||||
|
||||
if ($OpenFirewall) {
|
||||
$port = ($Urls -split ':')[-1]
|
||||
if ($port -match '^(\d+)$') {
|
||||
$ruleName = "$ServiceName HTTP $port"
|
||||
$existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
|
||||
if ($null -eq $existingRule) {
|
||||
Write-Host "Creating firewall rule for TCP port $port ..."
|
||||
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol TCP -LocalPort $port -Action Allow | Out-Null
|
||||
} else {
|
||||
Write-Host "Firewall rule '$ruleName' already exists, skipping creation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Start-ServiceSafe -name $ServiceName
|
||||
|
||||
Write-Host "Deployment complete. Service '$ServiceName' is running."
|
||||
Reference in New Issue
Block a user