diff --git a/ExportDXF/ApiClient/FabWorksApiDtos.cs b/ExportDXF/ApiClient/FabWorksApiDtos.cs
index 2893306..5e496c0 100644
--- a/ExportDXF/ApiClient/FabWorksApiDtos.cs
+++ b/ExportDXF/ApiClient/FabWorksApiDtos.cs
@@ -39,6 +39,7 @@ namespace ExportDXF.ApiClient
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
+ public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
diff --git a/FabWorks.Api/Controllers/BomItemsController.cs b/FabWorks.Api/Controllers/BomItemsController.cs
index be41f59..f6027d1 100644
--- a/FabWorks.Api/Controllers/BomItemsController.cs
+++ b/FabWorks.Api/Controllers/BomItemsController.cs
@@ -53,12 +53,16 @@ namespace FabWorks.Api.Controllers
var export = await _db.ExportRecords.FindAsync(exportId);
if (export == null) return NotFound("Export record not found");
- // Look for existing BomItem with same PartName + ConfigurationName under the same drawing
+ // Look up the latest CutTemplate for this drawing+item across all previous exports
+ // to determine the revision number
+ var newContentHash = dto.CutTemplate?.ContentHash;
+ int revision = await ResolveRevisionAsync(export.DrawingNumber, dto.ItemNo, newContentHash);
+
+ // Look for existing BomItem with same PartName + ConfigurationName within this export record
var existing = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
- .Include(b => b.ExportRecord)
- .Where(b => b.ExportRecord.DrawingNumber == export.DrawingNumber
+ .Where(b => b.ExportRecordId == exportId
&& b.PartName == (dto.PartName ?? "")
&& b.ConfigurationName == (dto.ConfigurationName ?? ""))
.OrderByDescending(b => b.ID)
@@ -66,8 +70,7 @@ namespace FabWorks.Api.Controllers
if (existing != null)
{
- // Update existing: move to new export record and refresh fields
- existing.ExportRecordId = exportId;
+ // Update existing fields
existing.PartNo = dto.PartNo ?? "";
existing.SortOrder = dto.SortOrder;
existing.Qty = dto.Qty;
@@ -81,6 +84,7 @@ namespace FabWorks.Api.Controllers
{
existing.CutTemplate.DxfFilePath = dto.CutTemplate.DxfFilePath ?? "";
existing.CutTemplate.ContentHash = dto.CutTemplate.ContentHash;
+ existing.CutTemplate.Revision = revision;
existing.CutTemplate.Thickness = dto.CutTemplate.Thickness;
existing.CutTemplate.KFactor = dto.CutTemplate.KFactor;
existing.CutTemplate.DefaultBendRadius = dto.CutTemplate.DefaultBendRadius;
@@ -91,6 +95,7 @@ namespace FabWorks.Api.Controllers
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
+ Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
@@ -156,6 +161,7 @@ namespace FabWorks.Api.Controllers
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
+ Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
@@ -185,6 +191,33 @@ namespace FabWorks.Api.Controllers
return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item));
}
+ ///
+ /// Determines the revision number for a CutTemplate by looking at the most recent
+ /// CutTemplate for the same drawing number and item number across all exports.
+ /// Returns 1 if no previous version exists, the same revision if the hash matches,
+ /// or previous revision + 1 if the hash changed.
+ ///
+ private async Task ResolveRevisionAsync(string drawingNumber, string itemNo, string contentHash)
+ {
+ if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo) || string.IsNullOrEmpty(contentHash))
+ return 1;
+
+ var previous = await _db.CutTemplates
+ .Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
+ && c.BomItem.ItemNo == itemNo
+ && c.ContentHash != null)
+ .OrderByDescending(c => c.Id)
+ .Select(c => new { c.ContentHash, c.Revision })
+ .FirstOrDefaultAsync();
+
+ if (previous == null)
+ return 1;
+
+ return previous.ContentHash == contentHash
+ ? previous.Revision
+ : previous.Revision + 1;
+ }
+
private static BomItemDto MapToDto(BomItem b) => new()
{
ID = b.ID,
@@ -202,6 +235,7 @@ namespace FabWorks.Api.Controllers
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
+ Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
diff --git a/FabWorks.Api/Controllers/ExportsController.cs b/FabWorks.Api/Controllers/ExportsController.cs
index 1873aee..e1d9a48 100644
--- a/FabWorks.Api/Controllers/ExportsController.cs
+++ b/FabWorks.Api/Controllers/ExportsController.cs
@@ -239,6 +239,7 @@ namespace FabWorks.Api.Controllers
Id = ct.Id,
DxfFilePath = ct.DxfFilePath,
ContentHash = ct.ContentHash,
+ Revision = ct.Revision,
Thickness = ct.Thickness,
KFactor = ct.KFactor,
DefaultBendRadius = ct.DefaultBendRadius
@@ -324,6 +325,7 @@ namespace FabWorks.Api.Controllers
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
+ Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
diff --git a/FabWorks.Api/DTOs/ExportDetailDto.cs b/FabWorks.Api/DTOs/ExportDetailDto.cs
index 870ae52..ed6eb78 100644
--- a/FabWorks.Api/DTOs/ExportDetailDto.cs
+++ b/FabWorks.Api/DTOs/ExportDetailDto.cs
@@ -39,6 +39,7 @@ namespace FabWorks.Api.DTOs
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
+ public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
diff --git a/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
new file mode 100644
index 0000000..59ad995
--- /dev/null
+++ b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
@@ -0,0 +1,273 @@
+//
+using System;
+using FabWorks.Core.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FabWorks.Core.Migrations
+{
+ [DbContext(typeof(FabWorksDbContext))]
+ [Migration("20260219134027_AddCutTemplateRevision")]
+ partial class AddCutTemplateRevision
+ {
+ ///
+ 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("FabWorks.Core.Models.BomItem", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID"));
+
+ b.Property("ConfigurationName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("ExportRecordId")
+ .HasColumnType("int");
+
+ b.Property("ItemNo")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Material")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PartName")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("PartNo")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Qty")
+ .HasColumnType("int");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("TotalQty")
+ .HasColumnType("int");
+
+ b.HasKey("ID");
+
+ b.HasIndex("ExportRecordId");
+
+ b.ToTable("BomItems");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BomItemId")
+ .HasColumnType("int");
+
+ b.Property("ContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CutTemplateName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("DefaultBendRadius")
+ .HasColumnType("float");
+
+ b.Property("DxfFilePath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("KFactor")
+ .HasColumnType("float");
+
+ b.Property("Revision")
+ .HasColumnType("int");
+
+ b.Property("Thickness")
+ .HasColumnType("float");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BomItemId")
+ .IsUnique();
+
+ b.ToTable("CutTemplates");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DrawingNo")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("DrawingNumber")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("EquipmentNo")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ExportedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ExportedBy")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("OutputFolder")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PdfContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SourceFilePath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Title")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ExportRecords");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BendCount")
+ .HasColumnType("int");
+
+ b.Property("BomItemId")
+ .HasColumnType("int");
+
+ b.Property("ContentHash")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("KFactor")
+ .HasColumnType("float");
+
+ b.Property("LowerToolNames")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("MaterialType")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProgramFilePath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("ProgramName")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SetupNotes")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("Thickness")
+ .HasColumnType("float");
+
+ b.Property("UpperToolNames")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BomItemId")
+ .IsUnique();
+
+ b.ToTable("FormPrograms");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
+ {
+ b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
+ .WithMany("BomItems")
+ .HasForeignKey("ExportRecordId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ExportRecord");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
+ {
+ b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
+ .WithOne("CutTemplate")
+ .HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("BomItem");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
+ {
+ b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
+ .WithOne("FormProgram")
+ .HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("BomItem");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
+ {
+ b.Navigation("CutTemplate");
+
+ b.Navigation("FormProgram");
+ });
+
+ modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
+ {
+ b.Navigation("BomItems");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs
new file mode 100644
index 0000000..4f8d959
--- /dev/null
+++ b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FabWorks.Core.Migrations
+{
+ ///
+ public partial class AddCutTemplateRevision : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Revision",
+ table: "CutTemplates",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Revision",
+ table: "CutTemplates");
+ }
+ }
+}
diff --git a/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs b/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs
index 72ce3fb..c6ebca9 100644
--- a/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs
+++ b/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs
@@ -102,6 +102,9 @@ namespace FabWorks.Core.Migrations
b.Property("KFactor")
.HasColumnType("float");
+ b.Property("Revision")
+ .HasColumnType("int");
+
b.Property("Thickness")
.HasColumnType("float");
diff --git a/FabWorks.Core/Models/CutTemplate.cs b/FabWorks.Core/Models/CutTemplate.cs
index 9a268fb..db0c9eb 100644
--- a/FabWorks.Core/Models/CutTemplate.cs
+++ b/FabWorks.Core/Models/CutTemplate.cs
@@ -16,6 +16,8 @@ namespace FabWorks.Core.Models
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
+ public int Revision { get; set; } = 1;
+
public double? KFactor { get; set; }
private double? _defaultBendRadius;