From c5f366a3efcea65da3e7fa8d708866a25a6c2cea Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 9 Feb 2026 22:12:38 -0500 Subject: [PATCH] feat: Add optimization result persistence to Job entity Add OptimizationResultJson and OptimizedAt columns to Job table. JobService now saves/clears optimization results and auto-clears stale results when parts, stock, or cutting tool change. Co-Authored-By: Claude Opus 4.6 --- CutList.Web/Data/ApplicationDbContext.cs | 2 + CutList.Web/Data/Entities/Job.cs | 2 + ...22312_AddJobOptimizationResult.Designer.cs | 905 ++++++++++++++++++ ...20260209122312_AddJobOptimizationResult.cs | 39 + .../ApplicationDbContextModelSnapshot.cs | 6 + CutList.Web/Services/JobService.cs | 39 +- 6 files changed, 992 insertions(+), 1 deletion(-) create mode 100644 CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.Designer.cs create mode 100644 CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.cs diff --git a/CutList.Web/Data/ApplicationDbContext.cs b/CutList.Web/Data/ApplicationDbContext.cs index 9c8c430..3edeeb5 100644 --- a/CutList.Web/Data/ApplicationDbContext.cs +++ b/CutList.Web/Data/ApplicationDbContext.cs @@ -234,6 +234,8 @@ public class ApplicationDbContext : DbContext entity.Property(e => e.Customer).HasMaxLength(100); entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()"); + entity.Property(e => e.OptimizationResultJson).HasColumnType("nvarchar(max)"); + entity.HasIndex(e => e.JobNumber).IsUnique(); entity.HasOne(e => e.CuttingTool) diff --git a/CutList.Web/Data/Entities/Job.cs b/CutList.Web/Data/Entities/Job.cs index 72d49e0..d4a1bab 100644 --- a/CutList.Web/Data/Entities/Job.cs +++ b/CutList.Web/Data/Entities/Job.cs @@ -11,6 +11,8 @@ public class Job public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } public DateTime? LockedAt { get; set; } + public string? OptimizationResultJson { get; set; } + public DateTime? OptimizedAt { get; set; } public bool IsLocked => LockedAt.HasValue; diff --git a/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.Designer.cs b/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.Designer.cs new file mode 100644 index 0000000..2f68bed --- /dev/null +++ b/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.Designer.cs @@ -0,0 +1,905 @@ +// +using System; +using CutList.Web.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CutList.Web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260209122312_AddJobOptimizationResult")] + partial class AddJobOptimizationResult + { + /// + 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("DimensionType") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("nvarchar(21)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId") + .IsUnique(); + + b.ToTable("MaterialDimensions"); + + b.HasDiscriminator("DimensionType").HasValue("MaterialDimensions"); + + b.UseTphMappingStrategy(); + }); + + 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") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Thickness"); + + b.HasIndex("Leg1"); + + b.HasDiscriminator().HasValue("Angle"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Flange") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Height") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Height"); + + b.Property("Web") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.HasIndex("Height"); + + b.HasDiscriminator().HasValue("Channel"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Thickness") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Thickness"); + + b.Property("Width") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Width"); + + b.HasIndex("Width"); + + b.HasDiscriminator().HasValue("FlatBar"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Height") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Height"); + + b.Property("WeightPerFoot") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.HasIndex("Height"); + + b.HasDiscriminator().HasValue("IBeam"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("NominalSize") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Schedule") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Wall") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Wall"); + + b.HasIndex("NominalSize"); + + b.HasDiscriminator().HasValue("Pipe"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Height") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Height"); + + b.Property("Wall") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Wall"); + + b.Property("Width") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Width"); + + b.HasIndex("Width"); + + b.HasDiscriminator().HasValue("RectangularTube"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Diameter") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.HasIndex("Diameter"); + + b.HasDiscriminator().HasValue("RoundBar"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("OuterDiameter") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("Wall") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Wall"); + + b.HasIndex("OuterDiameter"); + + b.HasDiscriminator().HasValue("RoundTube"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Size") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Size"); + + b.HasIndex("Size"); + + b.HasDiscriminator().HasValue("SquareBar"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b => + { + b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions"); + + b.Property("Size") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Size"); + + b.Property("Wall") + .ValueGeneratedOnUpdateSometimes() + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)") + .HasColumnName("Wall"); + + b.HasIndex("Size"); + + b.HasDiscriminator().HasValue("SquareTube"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool") + .WithMany("Jobs") + .HasForeignKey("CuttingToolId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CuttingTool"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany("Parts") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("JobParts") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany("Stock") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany() + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany() + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Job"); + + b.Navigation("Material"); + + b.Navigation("StockItem"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b => + { + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithOne("Dimensions") + .HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany() + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany() + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Job"); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("StockItems") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("Transactions") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany() + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Job"); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("SupplierOfferings") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany("Offerings") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.Navigation("Parts"); + + b.Navigation("Stock"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Material", b => + { + b.Navigation("Dimensions"); + + b.Navigation("JobParts"); + + b.Navigation("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.Navigation("SupplierOfferings"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b => + { + b.Navigation("Offerings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.cs b/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.cs new file mode 100644 index 0000000..1704287 --- /dev/null +++ b/CutList.Web/Migrations/20260209122312_AddJobOptimizationResult.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class AddJobOptimizationResult : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OptimizationResultJson", + table: "Jobs", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "OptimizedAt", + table: "Jobs", + type: "datetime2", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OptimizationResultJson", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "OptimizedAt", + table: "Jobs"); + } + } +} diff --git a/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs b/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs index 5351370..0139371 100644 --- a/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs @@ -119,6 +119,12 @@ namespace CutList.Web.Migrations b.Property("Notes") .HasColumnType("nvarchar(max)"); + b.Property("OptimizationResultJson") + .HasColumnType("nvarchar(max)"); + + b.Property("OptimizedAt") + .HasColumnType("datetime2"); + b.Property("UpdatedAt") .HasColumnType("datetime2"); diff --git a/CutList.Web/Services/JobService.cs b/CutList.Web/Services/JobService.cs index 9b47167..ed6f36e 100644 --- a/CutList.Web/Services/JobService.cs +++ b/CutList.Web/Services/JobService.cs @@ -72,6 +72,8 @@ public class JobService public async Task UpdateAsync(Job job) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; _context.Jobs.Update(job); await _context.SaveChangesAsync(); } @@ -161,6 +163,29 @@ public class JobService return duplicate; } + // Optimization result persistence + public async Task SaveOptimizationResultAsync(int jobId, string resultJson, DateTime optimizedAt) + { + var job = await _context.Jobs.FindAsync(jobId); + if (job != null) + { + job.OptimizationResultJson = resultJson; + job.OptimizedAt = optimizedAt; + await _context.SaveChangesAsync(); + } + } + + public async Task ClearOptimizationResultAsync(int jobId) + { + var job = await _context.Jobs.FindAsync(jobId); + if (job != null && job.OptimizationResultJson != null) + { + job.OptimizationResultJson = null; + job.OptimizedAt = null; + await _context.SaveChangesAsync(); + } + } + // Parts management public async Task AddPartAsync(JobPart part) { @@ -172,11 +197,13 @@ public class JobService _context.JobParts.Add(part); await _context.SaveChangesAsync(); - // Update job timestamp + // Update job timestamp and clear stale results var job = await _context.Jobs.FindAsync(part.JobId); if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } @@ -192,6 +219,8 @@ public class JobService if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } } @@ -209,6 +238,8 @@ public class JobService if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } } @@ -229,6 +260,8 @@ public class JobService if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } @@ -244,6 +277,8 @@ public class JobService if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } } @@ -261,6 +296,8 @@ public class JobService if (job != null) { job.UpdatedAt = DateTime.UtcNow; + job.OptimizationResultJson = null; + job.OptimizedAt = null; await _context.SaveChangesAsync(); } }