diff --git a/CLAUDE.md b/CLAUDE.md index bc91ed5..b2795ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` diff --git a/CutList.Web/Controllers/Dtos/SeedDataDtos.cs b/CutList.Web/Controllers/Dtos/SeedDataDtos.cs new file mode 100644 index 0000000..55f33b5 --- /dev/null +++ b/CutList.Web/Controllers/Dtos/SeedDataDtos.cs @@ -0,0 +1,72 @@ +using System.Text.Json.Serialization; + +namespace CutList.Web.Controllers.Dtos; + +public class SeedExportData +{ + public DateTime ExportedAt { get; set; } + public List Suppliers { get; set; } = []; + public List CuttingTools { get; set; } = []; + public List Materials { get; set; } = []; +} + +public class SeedSupplierDto +{ + public string Name { get; set; } = ""; + public string? ContactInfo { get; set; } + public string? Notes { get; set; } +} + +public class SeedCuttingToolDto +{ + public string Name { get; set; } = ""; + public decimal KerfInches { get; set; } + public bool IsDefault { get; set; } +} + +public class SeedMaterialDto +{ + public string Shape { get; set; } = ""; + public string Type { get; set; } = ""; + public string? Grade { get; set; } + public string Size { get; set; } = ""; + public string? Description { get; set; } + public SeedDimensionsDto? Dimensions { get; set; } + public List StockItems { get; set; } = []; +} + +public class SeedDimensionsDto +{ + public decimal? Diameter { get; set; } + public decimal? OuterDiameter { get; set; } + public decimal? Width { get; set; } + public decimal? Height { get; set; } + public decimal? Thickness { get; set; } + public decimal? Wall { get; set; } + public decimal? Size { get; set; } + public decimal? Leg1 { get; set; } + public decimal? Leg2 { get; set; } + public decimal? Flange { get; set; } + public decimal? Web { get; set; } + public decimal? WeightPerFoot { get; set; } + public decimal? NominalSize { get; set; } + public string? Schedule { get; set; } +} + +public class SeedStockItemDto +{ + public decimal LengthInches { get; set; } + public string? Name { get; set; } + public int QuantityOnHand { get; set; } + public string? Notes { get; set; } + public List SupplierOfferings { get; set; } = []; +} + +public class SeedSupplierOfferingDto +{ + 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; } +} diff --git a/CutList.Web/Controllers/SeedController.cs b/CutList.Web/Controllers/SeedController.cs new file mode 100644 index 0000000..2b84fca --- /dev/null +++ b/CutList.Web/Controllers/SeedController.cs @@ -0,0 +1,277 @@ +using CutList.Web.Controllers.Dtos; +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; + } + + [HttpGet("export")] + public async Task> Export() + { + 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 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 export = new SeedExportData + { + ExportedAt = DateTime.UtcNow, + Suppliers = suppliers.Select(s => new SeedSupplierDto + { + Name = s.Name, + ContactInfo = s.ContactInfo, + Notes = s.Notes + }).ToList(), + CuttingTools = cuttingTools.Select(t => new SeedCuttingToolDto + { + Name = t.Name, + KerfInches = t.KerfInches, + IsDefault = t.IsDefault + }).ToList(), + Materials = materials.Select(m => new SeedMaterialDto + { + Shape = m.Shape.ToString(), + Type = m.Type.ToString(), + Grade = m.Grade, + Size = m.Size, + Description = m.Description, + Dimensions = MapDimensionsToDto(m.Dimensions), + StockItems = m.StockItems.OrderBy(s => s.LengthInches).Select(s => new SeedStockItemDto + { + LengthInches = s.LengthInches, + Name = s.Name, + QuantityOnHand = s.QuantityOnHand, + Notes = s.Notes, + SupplierOfferings = s.SupplierOfferings.Select(o => new SeedSupplierOfferingDto + { + SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown", + PartNumber = o.PartNumber, + SupplierDescription = o.SupplierDescription, + Price = o.Price, + Notes = o.Notes + }).ToList() + }).ToList() + }).ToList() + }; + + return Ok(export); + } + + [HttpPost("import")] + public async Task Import([FromBody] SeedExportData data) + { + var suppliersCreated = 0; + var toolsCreated = 0; + var materialsCreated = 0; + var materialsSkipped = 0; + var stockCreated = 0; + var offeringsCreated = 0; + + // 1. Suppliers - match by name + var supplierMap = new Dictionary(); + foreach (var dto in data.Suppliers) + { + var existing = await _context.Suppliers.FirstOrDefaultAsync(s => s.Name == dto.Name); + if (existing != null) + { + supplierMap[dto.Name] = existing; + } + else + { + var supplier = new Supplier + { + Name = dto.Name, + ContactInfo = dto.ContactInfo, + Notes = dto.Notes, + CreatedAt = DateTime.UtcNow + }; + _context.Suppliers.Add(supplier); + supplierMap[dto.Name] = supplier; + suppliersCreated++; + } + } + await _context.SaveChangesAsync(); + + // 2. Cutting tools - match by name + foreach (var dto in data.CuttingTools) + { + var exists = await _context.CuttingTools.AnyAsync(t => t.Name == dto.Name); + if (!exists) + { + _context.CuttingTools.Add(new CuttingTool + { + Name = dto.Name, + KerfInches = dto.KerfInches, + IsDefault = dto.IsDefault + }); + toolsCreated++; + } + } + await _context.SaveChangesAsync(); + + // 3. Materials - match by shape + size + grade + foreach (var dto in data.Materials) + { + if (!Enum.TryParse(dto.Shape, out var shape)) + { + materialsSkipped++; + continue; + } + + Enum.TryParse(dto.Type, out var type); + + var existing = await _context.Materials + .Include(m => m.StockItems) + .FirstOrDefaultAsync(m => m.Shape == shape && m.Size == dto.Size && m.Grade == dto.Grade && m.IsActive); + + Material material; + if (existing != null) + { + material = existing; + materialsSkipped++; + } + else + { + material = new Material + { + Shape = shape, + Type = type, + Grade = dto.Grade, + Size = dto.Size, + Description = dto.Description, + CreatedAt = DateTime.UtcNow + }; + + if (dto.Dimensions != null) + material.Dimensions = MapDtoToDimensions(shape, dto.Dimensions); + + _context.Materials.Add(material); + await _context.SaveChangesAsync(); + materialsCreated++; + } + + // 4. Stock items - match by material + length + foreach (var stockDto in dto.StockItems) + { + var existingStock = material.StockItems + .FirstOrDefault(s => s.LengthInches == stockDto.LengthInches && s.IsActive); + + StockItem stockItem; + if (existingStock != null) + { + stockItem = existingStock; + } + else + { + stockItem = new StockItem + { + MaterialId = material.Id, + LengthInches = stockDto.LengthInches, + Name = stockDto.Name, + QuantityOnHand = stockDto.QuantityOnHand, + Notes = stockDto.Notes, + CreatedAt = DateTime.UtcNow + }; + _context.StockItems.Add(stockItem); + await _context.SaveChangesAsync(); + stockCreated++; + } + + // 5. Supplier offerings + foreach (var offeringDto in stockDto.SupplierOfferings) + { + if (!supplierMap.TryGetValue(offeringDto.SupplierName, out var supplier)) + continue; + + var existingOffering = await _context.SupplierOfferings + .AnyAsync(o => o.SupplierId == supplier.Id && o.StockItemId == stockItem.Id); + + if (!existingOffering) + { + _context.SupplierOfferings.Add(new SupplierOffering + { + SupplierId = supplier.Id, + StockItemId = stockItem.Id, + PartNumber = offeringDto.PartNumber, + SupplierDescription = offeringDto.SupplierDescription, + Price = offeringDto.Price, + Notes = offeringDto.Notes + }); + offeringsCreated++; + } + } + } + } + + await _context.SaveChangesAsync(); + + return Ok(new + { + Message = "Import completed", + SuppliersCreated = suppliersCreated, + CuttingToolsCreated = toolsCreated, + MaterialsCreated = materialsCreated, + MaterialsSkipped = materialsSkipped, + StockItemsCreated = stockCreated, + SupplierOfferingsCreated = offeringsCreated + }); + } + + private static SeedDimensionsDto? MapDimensionsToDto(MaterialDimensions? dim) => dim switch + { + RoundBarDimensions d => new SeedDimensionsDto { Diameter = d.Diameter }, + RoundTubeDimensions d => new SeedDimensionsDto { OuterDiameter = d.OuterDiameter, Wall = d.Wall }, + FlatBarDimensions d => new SeedDimensionsDto { Width = d.Width, Thickness = d.Thickness }, + SquareBarDimensions d => new SeedDimensionsDto { Size = d.Size }, + SquareTubeDimensions d => new SeedDimensionsDto { Size = d.Size, Wall = d.Wall }, + RectangularTubeDimensions d => new SeedDimensionsDto { Width = d.Width, Height = d.Height, Wall = d.Wall }, + AngleDimensions d => new SeedDimensionsDto { Leg1 = d.Leg1, Leg2 = d.Leg2, Thickness = d.Thickness }, + ChannelDimensions d => new SeedDimensionsDto { Height = d.Height, Flange = d.Flange, Web = d.Web }, + IBeamDimensions d => new SeedDimensionsDto { Height = d.Height, WeightPerFoot = d.WeightPerFoot }, + PipeDimensions d => new SeedDimensionsDto { NominalSize = d.NominalSize, Wall = d.Wall, Schedule = d.Schedule }, + _ => null + }; + + private static MaterialDimensions? MapDtoToDimensions(MaterialShape shape, SeedDimensionsDto dto) => shape switch + { + MaterialShape.RoundBar => new RoundBarDimensions { Diameter = dto.Diameter ?? 0 }, + MaterialShape.RoundTube => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter ?? 0, Wall = dto.Wall ?? 0 }, + MaterialShape.FlatBar => new FlatBarDimensions { Width = dto.Width ?? 0, Thickness = dto.Thickness ?? 0 }, + MaterialShape.SquareBar => new SquareBarDimensions { Size = dto.Size ?? 0 }, + MaterialShape.SquareTube => new SquareTubeDimensions { Size = dto.Size ?? 0, Wall = dto.Wall ?? 0 }, + MaterialShape.RectangularTube => new RectangularTubeDimensions { Width = dto.Width ?? 0, Height = dto.Height ?? 0, Wall = dto.Wall ?? 0 }, + MaterialShape.Angle => new AngleDimensions { Leg1 = dto.Leg1 ?? 0, Leg2 = dto.Leg2 ?? 0, Thickness = dto.Thickness ?? 0 }, + MaterialShape.Channel => new ChannelDimensions { Height = dto.Height ?? 0, Flange = dto.Flange ?? 0, Web = dto.Web ?? 0 }, + MaterialShape.IBeam => new IBeamDimensions { Height = dto.Height ?? 0, WeightPerFoot = dto.WeightPerFoot ?? 0 }, + MaterialShape.Pipe => new PipeDimensions { NominalSize = dto.NominalSize ?? 0, Wall = dto.Wall ?? 0, Schedule = dto.Schedule }, + _ => null + }; +} diff --git a/CutList.Web/CutList.Web.csproj b/CutList.Web/CutList.Web.csproj index 000a2ce..e2ed2c1 100644 --- a/CutList.Web/CutList.Web.csproj +++ b/CutList.Web/CutList.Web.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CutList.Web/Data/ApplicationDbContext.cs b/CutList.Web/Data/ApplicationDbContext.cs index 3edeeb5..7319a2d 100644 --- a/CutList.Web/Data/ApplicationDbContext.cs +++ b/CutList.Web/Data/ApplicationDbContext.cs @@ -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(entity => { entity.HasKey(e => e.Id); + entity.UseTpcMappingStrategy(); // 1:1 relationship with Material entity.HasOne(e => e.Material) .WithOne(m => m.Dimensions) .HasForeignKey(e => e.MaterialId) .OnDelete(DeleteBehavior.Cascade); - - // TPH discriminator - entity.HasDiscriminator("DimensionType") - .HasValue("RoundBar") - .HasValue("RoundTube") - .HasValue("FlatBar") - .HasValue("SquareBar") - .HasValue("SquareTube") - .HasValue("RectangularTube") - .HasValue("Angle") - .HasValue("Channel") - .HasValue("IBeam") - .HasValue("Pipe"); }); // Configure each dimension type's properties modelBuilder.Entity(entity => { + entity.ToTable("DimRoundBar"); entity.Property(e => e.Diameter).HasPrecision(10, 4); entity.HasIndex(e => e.Diameter); }); modelBuilder.Entity(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(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(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(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(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(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(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(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(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); }); diff --git a/CutList.Web/Data/SeedData/alro-catalog.json b/CutList.Web/Data/SeedData/alro-catalog.json new file mode 100644 index 0000000..b4cf814 --- /dev/null +++ b/CutList.Web/Data/SeedData/alro-catalog.json @@ -0,0 +1,31 @@ +{ + "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": [] +} \ No newline at end of file diff --git a/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.Designer.cs b/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.Designer.cs new file mode 100644 index 0000000..911629d --- /dev/null +++ b/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.Designer.cs @@ -0,0 +1,962 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("KerfInches") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Customer") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CuttingToolId") + .HasColumnType("int"); + + b.Property("JobNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LockedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizationResultJson") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizedAt") + .HasColumnType("datetime2"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsCustomLength") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Grade") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Shape") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Size") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Materials"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("QuantityOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId", "LengthInches") + .IsUnique(); + + b.ToTable("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactInfo") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PartNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierDescription") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("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("Leg1") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Leg2") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Flange") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Thickness") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("NominalSize") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Schedule") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Wall") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("OuterDiameter") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("Size") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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 + } + } +} diff --git a/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.cs b/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.cs new file mode 100644 index 0000000..673d46b --- /dev/null +++ b/CutList.Web/Migrations/20260216183131_MaterialDimensionsTPHtoTPT.cs @@ -0,0 +1,353 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class MaterialDimensionsTPHtoTPT : Migration + { + /// + 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(type: "int", nullable: false), + Leg1 = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Leg2 = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Thickness = table.Column(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(type: "int", nullable: false), + Height = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Flange = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Web = table.Column(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(type: "int", nullable: false), + Width = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Thickness = table.Column(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(type: "int", nullable: false), + Height = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + WeightPerFoot = table.Column(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(type: "int", nullable: false), + NominalSize = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Wall = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: true), + Schedule = table.Column(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(type: "int", nullable: false), + Width = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Height = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Wall = table.Column(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(type: "int", nullable: false), + Diameter = table.Column(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(type: "int", nullable: false), + OuterDiameter = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Wall = table.Column(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(type: "int", nullable: false), + Size = table.Column(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(type: "int", nullable: false), + Size = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Wall = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Re-add the TPH columns + migrationBuilder.AddColumn(name: "DimensionType", table: "MaterialDimensions", type: "nvarchar(21)", maxLength: 21, nullable: false, defaultValue: ""); + migrationBuilder.AddColumn(name: "Diameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Flange", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Height", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Leg1", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Leg2", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "NominalSize", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "OuterDiameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Schedule", table: "MaterialDimensions", type: "nvarchar(20)", maxLength: 20, nullable: true); + migrationBuilder.AddColumn(name: "Size", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Thickness", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Wall", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "Web", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(name: "WeightPerFoot", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true); + migrationBuilder.AddColumn(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"); + } + } +} diff --git a/CutList.Web/Migrations/20260216190925_RenameDimensionTables.Designer.cs b/CutList.Web/Migrations/20260216190925_RenameDimensionTables.Designer.cs new file mode 100644 index 0000000..8c2f89d --- /dev/null +++ b/CutList.Web/Migrations/20260216190925_RenameDimensionTables.Designer.cs @@ -0,0 +1,962 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("KerfInches") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Customer") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CuttingToolId") + .HasColumnType("int"); + + b.Property("JobNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LockedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizationResultJson") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizedAt") + .HasColumnType("datetime2"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsCustomLength") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Grade") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Shape") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Size") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Materials"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("QuantityOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId", "LengthInches") + .IsUnique(); + + b.ToTable("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactInfo") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PartNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierDescription") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("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("Leg1") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Leg2") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Flange") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Thickness") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("NominalSize") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Schedule") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Wall") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("OuterDiameter") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("Size") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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 + } + } +} diff --git a/CutList.Web/Migrations/20260216190925_RenameDimensionTables.cs b/CutList.Web/Migrations/20260216190925_RenameDimensionTables.cs new file mode 100644 index 0000000..985731c --- /dev/null +++ b/CutList.Web/Migrations/20260216190925_RenameDimensionTables.cs @@ -0,0 +1,678 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class RenameDimensionTables : Migration + { + /// + 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); + } + + /// + 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); + } + } +} diff --git a/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.Designer.cs b/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.Designer.cs new file mode 100644 index 0000000..235303b --- /dev/null +++ b/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.Designer.cs @@ -0,0 +1,875 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("KerfInches") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Customer") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CuttingToolId") + .HasColumnType("int"); + + b.Property("JobNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LockedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizationResultJson") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizedAt") + .HasColumnType("datetime2"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsCustomLength") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Grade") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Shape") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Size") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Materials"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id")); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("QuantityOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId", "LengthInches") + .IsUnique(); + + b.ToTable("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactInfo") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PartNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierDescription") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("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("Leg1") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Leg2") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Flange") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Thickness") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("NominalSize") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Schedule") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("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("Height") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Wall") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("OuterDiameter") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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("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("Size") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("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 + } + } +} diff --git a/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.cs b/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.cs new file mode 100644 index 0000000..90fabdd --- /dev/null +++ b/CutList.Web/Migrations/20260216191345_DimensionsTPTtoTPC.cs @@ -0,0 +1,172 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class DimensionsTPTtoTPC : Migration + { + private static readonly string[] DimTables = + [ + "DimAngle", "DimChannel", "DimFlatBar", "DimIBeam", "DimPipe", + "DimRectangularTube", "DimRoundBar", "DimRoundTube", "DimSquareBar", "DimSquareTube" + ]; + + /// + 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( + 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( + 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( + 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); + } + } + + /// + 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( + 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(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + MaterialId = table.Column(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); + } + } + } +} diff --git a/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs b/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs index 0139371..38a2d91 100644 --- a/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,8 @@ namespace CutList.Web.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.HasSequence("MaterialDimensionsSequence"); + modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b => { b.Property("Id") @@ -274,14 +276,10 @@ namespace CutList.Web.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("DimensionType") - .IsRequired() - .HasMaxLength(21) - .HasColumnType("nvarchar(21)"); + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id")); b.Property("MaterialId") .HasColumnType("int"); @@ -291,11 +289,9 @@ namespace CutList.Web.Migrations b.HasIndex("MaterialId") .IsUnique(); - b.ToTable("MaterialDimensions"); + b.ToTable((string)null); - b.HasDiscriminator("DimensionType").HasValue("MaterialDimensions"); - - b.UseTphMappingStrategy(); + b.UseTpcMappingStrategy(); }); modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b => @@ -527,14 +523,12 @@ namespace CutList.Web.Migrations .HasColumnType("decimal(10,4)"); b.Property("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 => @@ -546,10 +540,8 @@ namespace CutList.Web.Migrations .HasColumnType("decimal(10,4)"); b.Property("Height") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Height"); + .HasColumnType("decimal(10,4)"); b.Property("Web") .HasPrecision(10, 4) @@ -557,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 => @@ -565,20 +557,16 @@ namespace CutList.Web.Migrations b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); b.Property("Thickness") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Thickness"); + .HasColumnType("decimal(10,4)"); b.Property("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 => @@ -586,10 +574,8 @@ namespace CutList.Web.Migrations b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); b.Property("Height") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Height"); + .HasColumnType("decimal(10,4)"); b.Property("WeightPerFoot") .HasPrecision(10, 4) @@ -597,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 => @@ -613,14 +599,12 @@ namespace CutList.Web.Migrations .HasColumnType("nvarchar(20)"); b.Property("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 => @@ -628,26 +612,20 @@ namespace CutList.Web.Migrations b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); b.Property("Height") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Height"); + .HasColumnType("decimal(10,4)"); b.Property("Wall") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Wall"); + .HasColumnType("decimal(10,4)"); b.Property("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 => @@ -660,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 => @@ -672,14 +650,12 @@ namespace CutList.Web.Migrations .HasColumnType("decimal(10,4)"); b.Property("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 => @@ -687,14 +663,12 @@ namespace CutList.Web.Migrations b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); b.Property("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 => @@ -702,20 +676,16 @@ namespace CutList.Web.Migrations b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); b.Property("Size") - .ValueGeneratedOnUpdateSometimes() .HasPrecision(10, 4) - .HasColumnType("decimal(10,4)") - .HasColumnName("Size"); + .HasColumnType("decimal(10,4)"); b.Property("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 => diff --git a/CutList.Web/Program.cs b/CutList.Web/Program.cs index 38f194d..7a5ffd8 100644 --- a/CutList.Web/Program.cs +++ b/CutList.Web/Program.cs @@ -10,6 +10,9 @@ builder.Services.AddControllers(); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + // Add Entity Framework builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); @@ -27,7 +30,12 @@ builder.Services.AddScoped(); 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(); diff --git a/scripts/AlroCatalog/SCRAPE_PLAN.md b/scripts/AlroCatalog/SCRAPE_PLAN.md new file mode 100644 index 0000000..aca41f3 --- /dev/null +++ b/scripts/AlroCatalog/SCRAPE_PLAN.md @@ -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. diff --git a/scripts/AlroCatalog/alro-scrape-progress.json b/scripts/AlroCatalog/alro-scrape-progress.json new file mode 100644 index 0000000..a7fec7c --- /dev/null +++ b/scripts/AlroCatalog/alro-scrape-progress.json @@ -0,0 +1,971 @@ +{ + "completed": [ + [ + "Bars", + "A-36", + "ROUND" + ] + ], + "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 + } + ] +} \ No newline at end of file diff --git a/scripts/AlroCatalog/scrape_alro.py b/scripts/AlroCatalog/scrape_alro.py new file mode 100644 index 0000000..48bef4b --- /dev/null +++ b/scripts/AlroCatalog/scrape_alro.py @@ -0,0 +1,728 @@ +#!/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 (edit GRADE_FILTER below) + 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 --resume # Resume from 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