diff --git a/docs/plans/2026-02-17-autofill-from-export-history-design.md b/docs/plans/2026-02-17-autofill-from-export-history-design.md new file mode 100644 index 0000000..18b6c4e --- /dev/null +++ b/docs/plans/2026-02-17-autofill-from-export-history-design.md @@ -0,0 +1,32 @@ +# Auto-fill Equipment/Drawing from Export History + +## Problem + +When a SolidWorks file is opened that doesn't match the `DrawingInfo` regex (e.g., `Conveyor Frame.sldasm` instead of `5028 A02 Conveyor.slddrw`), the equipment and drawing number dropdowns are left empty even though the file may have been exported before with known values. + +## Decision + +- **Lookup key:** SolidWorks source file path (`ActiveDocument.FilePath`) +- **Storage:** Query the existing `ExportRecords` table (no new table or migration) +- **Priority:** Database lookup first; fall back to title regex parse if no history found + +## Design + +### Modify `UpdateActiveDocumentDisplay()` in `MainForm.cs` + +When the active document changes: + +1. Query the DB for the most recent `ExportRecord` where `SourceFilePath` matches `activeDoc.FilePath` (case-insensitive) +2. If found, parse the stored `DrawingNumber` via `DrawingInfo.Parse()` and auto-fill equipment/drawing dropdowns +3. If not found, fall back to current behavior: `DrawingInfo.Parse(activeDoc.Title)` + +### What doesn't change + +- No schema changes, no new migration +- Equipment/drawing dropdowns still populated with historical values in `InitializeDrawingDropdowns()` +- Export flow untouched + +### Error handling + +- DB query wrapped in try/catch so failures don't break the UI +- Case-insensitive path comparison (Windows paths are case-insensitive) diff --git a/docs/plans/2026-02-17-fabworks-api.md b/docs/plans/2026-02-17-fabworks-api.md new file mode 100644 index 0000000..8109625 --- /dev/null +++ b/docs/plans/2026-02-17-fabworks-api.md @@ -0,0 +1,1191 @@ +# FabWorks.Api Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Create a standalone ASP.NET Web API (`FabWorks.Api`) that owns all fabrication data — export records, BOM items, cut templates, form programs — so ExportDXF and other tools communicate via HTTP instead of direct database access. + +**Architecture:** FabWorks.Api is a minimal ASP.NET Web API targeting net8.0. It uses EF Core with SQL Server (same database ExportDXF already uses). A shared class library (`FabWorks.Core`) holds the entity models and DbContext so both the API and ExportDXF can share the schema during the migration period. CincyLib's press brake models are copied into FabWorks.Core as a net8.0 port (just the PressBrake folder — pure XML parsing, no dependencies). The API exposes RESTful endpoints for export records, BOM items, cut templates, and form programs. + +**Tech Stack:** ASP.NET Web API (net8.0), EF Core 8 + SQL Server, CincyLib PressBrake parser (ported to net8.0), xUnit for tests + +--- + +## Project Layout + +``` +ExportDXF/ (solution root) +├── ExportDXF.sln (add new projects here) +├── ExportDXF/ (existing WinForms app) +├── FabWorks.Core/ ← NEW shared library +│ ├── FabWorks.Core.csproj +│ ├── Data/ +│ │ └── FabWorksDbContext.cs +│ ├── Models/ +│ │ ├── ExportRecord.cs +│ │ ├── BomItem.cs +│ │ ├── CutTemplate.cs +│ │ └── FormProgram.cs ← NEW entity +│ └── PressBrake/ ← Ported from CincyLib +│ ├── Program.cs +│ ├── ProgramReader.cs +│ ├── Step.cs +│ ├── ToolSetup.cs +│ ├── SegEntry.cs +│ ├── MatType.cs +│ └── Extensions.cs +├── FabWorks.Api/ ← NEW web API +│ ├── FabWorks.Api.csproj +│ ├── Program.cs +│ ├── Controllers/ +│ │ ├── ExportsController.cs +│ │ ├── BomItemsController.cs +│ │ └── FormProgramsController.cs +│ ├── DTOs/ +│ │ ├── CreateExportRequest.cs +│ │ ├── ExportDetailDto.cs +│ │ ├── BomItemDto.cs +│ │ └── FormProgramDto.cs +│ └── Services/ +│ └── FormProgramService.cs +└── FabWorks.Tests/ ← NEW test project + ├── FabWorks.Tests.csproj + ├── FormProgramServiceTests.cs + ├── ExportsControllerTests.cs + └── PressBrake/ + └── ProgramReaderTests.cs +``` + +--- + +## Task 1: Create FabWorks.Core with Shared Models + +**Files:** +- Create: `FabWorks.Core/FabWorks.Core.csproj` +- Create: `FabWorks.Core/Models/ExportRecord.cs` +- Create: `FabWorks.Core/Models/BomItem.cs` +- Create: `FabWorks.Core/Models/CutTemplate.cs` +- Create: `FabWorks.Core/Models/FormProgram.cs` +- Create: `FabWorks.Core/Data/FabWorksDbContext.cs` + +**Step 1: Create the class library project** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF +dotnet new classlib -n FabWorks.Core --framework net8.0 +dotnet sln ExportDXF.sln add FabWorks.Core/FabWorks.Core.csproj +``` + +**Step 2: Add EF Core package to FabWorks.Core** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Core +dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.11 +dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.11 +``` + +**Step 3: Create entity models** + +Move/recreate the existing entity models from `ExportDXF/Models/` into `FabWorks.Core/Models/` under namespace `FabWorks.Core.Models`. Keep the exact same schema so the existing database is compatible. + +`FabWorks.Core/Models/ExportRecord.cs`: +```csharp +using System; +using System.Collections.Generic; + +namespace FabWorks.Core.Models +{ + public class ExportRecord + { + public int Id { get; set; } + public string DrawingNumber { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + public DateTime ExportedAt { get; set; } + public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } + + public virtual ICollection BomItems { get; set; } = new List(); + } +} +``` + +`FabWorks.Core/Models/BomItem.cs`: +```csharp +namespace FabWorks.Core.Models +{ + public class BomItem + { + public int ID { get; set; } + public string ItemNo { get; set; } = ""; + public string PartNo { get; set; } = ""; + public int SortOrder { get; set; } + public int? Qty { get; set; } + public int? TotalQty { get; set; } + public string Description { get; set; } = ""; + public string PartName { get; set; } = ""; + public string ConfigurationName { get; set; } = ""; + public string Material { get; set; } = ""; + + public int ExportRecordId { get; set; } + public virtual ExportRecord ExportRecord { get; set; } + + public virtual CutTemplate CutTemplate { get; set; } + public virtual FormProgram FormProgram { get; set; } + } +} +``` + +`FabWorks.Core/Models/CutTemplate.cs`: +```csharp +using System; + +namespace FabWorks.Core.Models +{ + public class CutTemplate + { + public int Id { get; set; } + public string DxfFilePath { get; set; } = ""; + public string ContentHash { get; set; } + public string CutTemplateName { get; set; } = ""; + + private double? _thickness; + public double? Thickness + { + get => _thickness; + set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null; + } + + public double? KFactor { get; set; } + + private double? _defaultBendRadius; + public double? DefaultBendRadius + { + get => _defaultBendRadius; + set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null; + } + + public int BomItemId { get; set; } + public virtual BomItem BomItem { get; set; } + } +} +``` + +`FabWorks.Core/Models/FormProgram.cs` (NEW): +```csharp +namespace FabWorks.Core.Models +{ + public class FormProgram + { + public int Id { get; set; } + public string ProgramFilePath { get; set; } = ""; + public string ContentHash { get; set; } + public string ProgramName { get; set; } = ""; + public double? Thickness { get; set; } + public string MaterialType { get; set; } = ""; + public double? KFactor { get; set; } + public int BendCount { get; set; } + public string UpperToolNames { get; set; } = ""; + public string LowerToolNames { get; set; } = ""; + public string SetupNotes { get; set; } = ""; + + public int BomItemId { get; set; } + public virtual BomItem BomItem { get; set; } + } +} +``` + +**Step 4: Create FabWorksDbContext** + +`FabWorks.Core/Data/FabWorksDbContext.cs`: +```csharp +using FabWorks.Core.Models; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Core.Data +{ + public class FabWorksDbContext : DbContext + { + public DbSet ExportRecords { get; set; } + public DbSet BomItems { get; set; } + public DbSet CutTemplates { get; set; } + public DbSet FormPrograms { get; set; } + + public FabWorksDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.DrawingNumber).HasMaxLength(100); + entity.Property(e => e.SourceFilePath).HasMaxLength(500); + entity.Property(e => e.OutputFolder).HasMaxLength(500); + entity.Property(e => e.ExportedBy).HasMaxLength(100); + entity.Property(e => e.PdfContentHash).HasMaxLength(64); + + entity.HasMany(e => e.BomItems) + .WithOne(b => b.ExportRecord) + .HasForeignKey(b => b.ExportRecordId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.ID); + entity.Property(e => e.ItemNo).HasMaxLength(50); + entity.Property(e => e.PartNo).HasMaxLength(100); + entity.Property(e => e.Description).HasMaxLength(500); + entity.Property(e => e.PartName).HasMaxLength(200); + entity.Property(e => e.ConfigurationName).HasMaxLength(100); + entity.Property(e => e.Material).HasMaxLength(100); + + entity.HasOne(e => e.CutTemplate) + .WithOne(ct => ct.BomItem) + .HasForeignKey(ct => ct.BomItemId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.FormProgram) + .WithOne(fp => fp.BomItem) + .HasForeignKey(fp => fp.BomItemId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.DxfFilePath).HasMaxLength(500); + entity.Property(e => e.CutTemplateName).HasMaxLength(100); + entity.Property(e => e.ContentHash).HasMaxLength(64); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.ProgramFilePath).HasMaxLength(500); + entity.Property(e => e.ContentHash).HasMaxLength(64); + entity.Property(e => e.ProgramName).HasMaxLength(200); + entity.Property(e => e.MaterialType).HasMaxLength(50); + entity.Property(e => e.UpperToolNames).HasMaxLength(500); + entity.Property(e => e.LowerToolNames).HasMaxLength(500); + entity.Property(e => e.SetupNotes).HasMaxLength(2000); + }); + } + } +} +``` + +**Step 5: Delete auto-generated Class1.cs and build** + +```bash +del FabWorks.Core\Class1.cs +dotnet build FabWorks.Core/FabWorks.Core.csproj +``` + +**Step 6: Commit** + +```bash +git add FabWorks.Core/ +git commit -m "feat: add FabWorks.Core shared library with entity models and FormProgram" +``` + +--- + +## Task 2: Port CincyLib PressBrake Parser into FabWorks.Core + +**Files:** +- Create: `FabWorks.Core/PressBrake/Program.cs` +- Create: `FabWorks.Core/PressBrake/ProgramReader.cs` +- Create: `FabWorks.Core/PressBrake/Step.cs` +- Create: `FabWorks.Core/PressBrake/ToolSetup.cs` +- Create: `FabWorks.Core/PressBrake/SegEntry.cs` +- Create: `FabWorks.Core/PressBrake/MatType.cs` +- Create: `FabWorks.Core/PressBrake/Extensions.cs` + +**Step 1: Copy CincyLib PressBrake files** + +Copy the following files from `C:\Users\aisaacs\Desktop\Projects\CincyLib\CincyLib\PressBrake\` into `FabWorks.Core\PressBrake\`: +- `Program.cs` +- `ProgramReader.cs` +- `Step.cs` +- `ToolSetup.cs` +- `SegEntry.cs` +- `MatType.cs` + +Also copy `C:\Users\aisaacs\Desktop\Projects\CincyLib\CincyLib\Extensions.cs` into `FabWorks.Core\PressBrake\Extensions.cs`. + +**Step 2: Update namespaces** + +Change all `namespace CincyLib.PressBrake` to `namespace FabWorks.Core.PressBrake`. +Change `namespace CincyLib` (in Extensions.cs) to `namespace FabWorks.Core.PressBrake`. + +No other code changes needed — these files only use `System`, `System.IO`, `System.Linq`, `System.Xml.Linq`, `System.Collections.Generic`, all available in net8.0. + +**Step 3: Build to verify port** + +```bash +dotnet build FabWorks.Core/FabWorks.Core.csproj +``` + +**Step 4: Commit** + +```bash +git add FabWorks.Core/PressBrake/ +git commit -m "feat: port CincyLib PressBrake parser to FabWorks.Core (net8.0)" +``` + +--- + +## Task 3: Create FabWorks.Tests and Validate PressBrake Parser + +**Files:** +- Create: `FabWorks.Tests/FabWorks.Tests.csproj` +- Create: `FabWorks.Tests/PressBrake/ProgramReaderTests.cs` +- Create: `FabWorks.Tests/TestData/` (copy a sample .pgm file) + +**Step 1: Create test project** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF +dotnet new xunit -n FabWorks.Tests --framework net8.0 +dotnet sln ExportDXF.sln add FabWorks.Tests/FabWorks.Tests.csproj +cd FabWorks.Tests +dotnet add reference ../FabWorks.Core/FabWorks.Core.csproj +``` + +**Step 2: Copy test data** + +Copy a sample `.pgm` file into `FabWorks.Tests/TestData/sample.pgm`. Use the file from `S:\4980 GMCH Lockport\4980 A05 Degreaser Casing\4980 A05-1 Access door\Forms\4980 A05-1 PT02.pgm`. Add it to the csproj as ``. + +**Step 3: Write ProgramReader test** + +`FabWorks.Tests/PressBrake/ProgramReaderTests.cs`: +```csharp +using FabWorks.Core.PressBrake; +using Xunit; + +namespace FabWorks.Tests.PressBrake +{ + public class ProgramReaderTests + { + [Fact] + public void Load_SamplePgm_ParsesProgramName() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.False(string.IsNullOrEmpty(pgm.ProgName)); + } + + [Fact] + public void Load_SamplePgm_ParsesThickness() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.True(pgm.MatThick > 0); + } + + [Fact] + public void Load_SamplePgm_ParsesSteps() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.NotEmpty(pgm.Steps); + } + + [Fact] + public void Load_SamplePgm_ParsesToolSetups() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.NotEmpty(pgm.UpperToolSets); + Assert.NotEmpty(pgm.LowerToolSets); + } + + [Fact] + public void Load_SamplePgm_ResolvesStepToolReferences() + { + var pgm = Program.Load("TestData/sample.pgm"); + var step = pgm.Steps[0]; + Assert.NotNull(step.UpperTool); + Assert.NotNull(step.LowerTool); + } + } +} +``` + +**Step 4: Run tests** + +```bash +dotnet test FabWorks.Tests/FabWorks.Tests.csproj -v normal +``` + +Expected: All 5 tests PASS. + +**Step 5: Commit** + +```bash +git add FabWorks.Tests/ +git commit -m "test: add ProgramReader tests validating CincyLib port" +``` + +--- + +## Task 4: Create FabWorks.Api Project with Exports Endpoint + +**Files:** +- Create: `FabWorks.Api/FabWorks.Api.csproj` +- Create: `FabWorks.Api/Program.cs` +- Create: `FabWorks.Api/Controllers/ExportsController.cs` +- Create: `FabWorks.Api/DTOs/CreateExportRequest.cs` +- Create: `FabWorks.Api/DTOs/ExportDetailDto.cs` + +**Step 1: Create web API project** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF +dotnet new webapi -n FabWorks.Api --framework net8.0 --no-openapi +dotnet sln ExportDXF.sln add FabWorks.Api/FabWorks.Api.csproj +cd FabWorks.Api +dotnet add reference ../FabWorks.Core/FabWorks.Core.csproj +``` + +**Step 2: Configure Program.cs with EF Core** + +`FabWorks.Api/Program.cs`: +```csharp +using FabWorks.Core.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb"))); + +var app = builder.Build(); + +app.MapControllers(); +app.Run(); +``` + +Add connection string to `appsettings.json`: +```json +{ + "ConnectionStrings": { + "FabWorksDb": "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;" + } +} +``` + +Note: Points at the same `ExportDxfDb` database. Same schema, just adding the FormPrograms table. + +**Step 3: Create DTOs** + +`FabWorks.Api/DTOs/CreateExportRequest.cs`: +```csharp +namespace FabWorks.Api.DTOs +{ + public class CreateExportRequest + { + public string DrawingNumber { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + } +} +``` + +`FabWorks.Api/DTOs/ExportDetailDto.cs`: +```csharp +using System; +using System.Collections.Generic; + +namespace FabWorks.Api.DTOs +{ + public class ExportDetailDto + { + public int Id { get; set; } + public string DrawingNumber { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + public DateTime ExportedAt { get; set; } + public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } + public List BomItems { get; set; } = new(); + } + + public class BomItemDto + { + public int ID { get; set; } + public string ItemNo { get; set; } + public string PartNo { get; set; } + public int SortOrder { get; set; } + public int? Qty { get; set; } + public int? TotalQty { get; set; } + public string Description { get; set; } + public string PartName { get; set; } + public string ConfigurationName { get; set; } + public string Material { get; set; } + public CutTemplateDto CutTemplate { get; set; } + public FormProgramDto FormProgram { get; set; } + } + + public class CutTemplateDto + { + public int Id { get; set; } + public string DxfFilePath { get; set; } + public string ContentHash { get; set; } + public double? Thickness { get; set; } + public double? KFactor { get; set; } + public double? DefaultBendRadius { get; set; } + } + + public class FormProgramDto + { + public int Id { get; set; } + public string ProgramFilePath { get; set; } + public string ContentHash { get; set; } + public string ProgramName { get; set; } + public double? Thickness { get; set; } + public string MaterialType { get; set; } + public double? KFactor { get; set; } + public int BendCount { get; set; } + public string UpperToolNames { get; set; } + public string LowerToolNames { get; set; } + public string SetupNotes { get; set; } + } +} +``` + +**Step 4: Create ExportsController** + +`FabWorks.Api/Controllers/ExportsController.cs`: +```csharp +using FabWorks.Api.DTOs; +using FabWorks.Core.Data; +using FabWorks.Core.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ExportsController : ControllerBase + { + private readonly FabWorksDbContext _db; + + public ExportsController(FabWorksDbContext db) + { + _db = db; + } + + [HttpPost] + public async Task> Create(CreateExportRequest request) + { + var record = new ExportRecord + { + DrawingNumber = request.DrawingNumber, + SourceFilePath = request.SourceFilePath, + OutputFolder = request.OutputFolder, + ExportedAt = DateTime.Now, + ExportedBy = Environment.UserName + }; + + _db.ExportRecords.Add(record); + await _db.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = record.Id }, MapToDto(record)); + } + + [HttpGet("{id}")] + public async Task> GetById(int id) + { + var record = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) + .FirstOrDefaultAsync(r => r.Id == id); + + if (record == null) return NotFound(); + return MapToDto(record); + } + + [HttpGet("by-source")] + public async Task> GetBySourceFile([FromQuery] string path) + { + var record = await _db.ExportRecords + .Where(r => r.SourceFilePath.ToLower() == path.ToLower() + && !string.IsNullOrEmpty(r.DrawingNumber)) + .OrderByDescending(r => r.Id) + .FirstOrDefaultAsync(); + + if (record == null) return NotFound(); + return MapToDto(record); + } + + [HttpGet("by-drawing")] + public async Task>> GetByDrawing([FromQuery] string drawingNumber) + { + var records = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) + .Where(r => r.DrawingNumber == drawingNumber) + .OrderByDescending(r => r.ExportedAt) + .ToListAsync(); + + return records.Select(MapToDto).ToList(); + } + + [HttpGet("next-item-number")] + public async Task> GetNextItemNumber([FromQuery] string drawingNumber) + { + if (string.IsNullOrEmpty(drawingNumber)) return "1"; + + var existingItems = await _db.ExportRecords + .Where(r => r.DrawingNumber == drawingNumber) + .SelectMany(r => r.BomItems) + .Select(b => b.ItemNo) + .ToListAsync(); + + int maxNum = 0; + foreach (var itemNo in existingItems) + { + if (int.TryParse(itemNo, out var num) && num > maxNum) + maxNum = num; + } + return (maxNum + 1).ToString(); + } + + private static ExportDetailDto MapToDto(ExportRecord r) => new() + { + Id = r.Id, + DrawingNumber = r.DrawingNumber, + SourceFilePath = r.SourceFilePath, + OutputFolder = r.OutputFolder, + ExportedAt = r.ExportedAt, + ExportedBy = r.ExportedBy, + PdfContentHash = r.PdfContentHash, + BomItems = r.BomItems?.Select(b => new BomItemDto + { + ID = b.ID, + ItemNo = b.ItemNo, + PartNo = b.PartNo, + SortOrder = b.SortOrder, + Qty = b.Qty, + TotalQty = b.TotalQty, + Description = b.Description, + PartName = b.PartName, + ConfigurationName = b.ConfigurationName, + Material = b.Material, + CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto + { + Id = b.CutTemplate.Id, + DxfFilePath = b.CutTemplate.DxfFilePath, + ContentHash = b.CutTemplate.ContentHash, + Thickness = b.CutTemplate.Thickness, + KFactor = b.CutTemplate.KFactor, + DefaultBendRadius = b.CutTemplate.DefaultBendRadius + }, + FormProgram = b.FormProgram == null ? null : new FormProgramDto + { + Id = b.FormProgram.Id, + ProgramFilePath = b.FormProgram.ProgramFilePath, + ContentHash = b.FormProgram.ContentHash, + ProgramName = b.FormProgram.ProgramName, + Thickness = b.FormProgram.Thickness, + MaterialType = b.FormProgram.MaterialType, + KFactor = b.FormProgram.KFactor, + BendCount = b.FormProgram.BendCount, + UpperToolNames = b.FormProgram.UpperToolNames, + LowerToolNames = b.FormProgram.LowerToolNames, + SetupNotes = b.FormProgram.SetupNotes + } + }).ToList() ?? new() + }; + } +} +``` + +**Step 5: Clean up auto-generated files, build** + +Delete the auto-generated `WeatherForecast.cs` and `Controllers/WeatherForecastController.cs` if present. + +```bash +dotnet build FabWorks.Api/FabWorks.Api.csproj +``` + +**Step 6: Commit** + +```bash +git add FabWorks.Api/ +git commit -m "feat: add FabWorks.Api with ExportsController and DTOs" +``` + +--- + +## Task 5: Add BomItems and FormPrograms Controllers + +**Files:** +- Create: `FabWorks.Api/Controllers/BomItemsController.cs` +- Create: `FabWorks.Api/Controllers/FormProgramsController.cs` +- Create: `FabWorks.Api/Services/FormProgramService.cs` + +**Step 1: Create FormProgramService (wraps CincyLib parser)** + +`FabWorks.Api/Services/FormProgramService.cs`: +```csharp +using FabWorks.Core.Models; +using FabWorks.Core.PressBrake; +using System.Security.Cryptography; + +namespace FabWorks.Api.Services +{ + public class FormProgramService + { + /// + /// Parse a .pgm file and return a FormProgram entity with metadata populated. + /// + public FormProgram ParseFromFile(string filePath) + { + var pgm = Program.Load(filePath); + var hash = ComputeFileHash(filePath); + + return new FormProgram + { + ProgramFilePath = filePath, + ContentHash = hash, + ProgramName = pgm.ProgName ?? "", + Thickness = pgm.MatThick > 0 ? pgm.MatThick : null, + MaterialType = pgm.MatType.ToString(), + KFactor = pgm.KFactor > 0 ? pgm.KFactor : null, + BendCount = pgm.Steps.Count, + UpperToolNames = string.Join(", ", pgm.UpperToolSets + .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), + LowerToolNames = string.Join(", ", pgm.LowerToolSets + .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), + SetupNotes = pgm.SetupNotes ?? "" + }; + } + + private static string ComputeFileHash(string filePath) + { + using var sha = SHA256.Create(); + using var stream = File.OpenRead(filePath); + var bytes = sha.ComputeHash(stream); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } + } +} +``` + +**Step 2: Register FormProgramService in DI** + +In `FabWorks.Api/Program.cs`, add: +```csharp +builder.Services.AddSingleton(); +``` + +**Step 3: Create BomItemsController** + +`FabWorks.Api/Controllers/BomItemsController.cs`: +```csharp +using FabWorks.Api.DTOs; +using FabWorks.Core.Data; +using FabWorks.Core.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/exports/{exportId}/bom-items")] + public class BomItemsController : ControllerBase + { + private readonly FabWorksDbContext _db; + + public BomItemsController(FabWorksDbContext db) => _db = db; + + [HttpGet] + public async Task>> GetByExport(int exportId) + { + var items = await _db.BomItems + .Include(b => b.CutTemplate) + .Include(b => b.FormProgram) + .Where(b => b.ExportRecordId == exportId) + .OrderBy(b => b.SortOrder) + .ToListAsync(); + + return items.Select(MapToDto).ToList(); + } + + [HttpPost] + public async Task> Create(int exportId, BomItemDto dto) + { + var export = await _db.ExportRecords.FindAsync(exportId); + if (export == null) return NotFound("Export record not found"); + + var item = new BomItem + { + ExportRecordId = exportId, + ItemNo = dto.ItemNo ?? "", + PartNo = dto.PartNo ?? "", + SortOrder = dto.SortOrder, + Qty = dto.Qty, + TotalQty = dto.TotalQty, + Description = dto.Description ?? "", + PartName = dto.PartName ?? "", + ConfigurationName = dto.ConfigurationName ?? "", + Material = dto.Material ?? "" + }; + + if (dto.CutTemplate != null) + { + item.CutTemplate = new CutTemplate + { + DxfFilePath = dto.CutTemplate.DxfFilePath ?? "", + ContentHash = dto.CutTemplate.ContentHash, + Thickness = dto.CutTemplate.Thickness, + KFactor = dto.CutTemplate.KFactor, + DefaultBendRadius = dto.CutTemplate.DefaultBendRadius + }; + } + + if (dto.FormProgram != null) + { + item.FormProgram = new FormProgram + { + ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "", + ContentHash = dto.FormProgram.ContentHash, + ProgramName = dto.FormProgram.ProgramName ?? "", + Thickness = dto.FormProgram.Thickness, + MaterialType = dto.FormProgram.MaterialType ?? "", + KFactor = dto.FormProgram.KFactor, + BendCount = dto.FormProgram.BendCount, + UpperToolNames = dto.FormProgram.UpperToolNames ?? "", + LowerToolNames = dto.FormProgram.LowerToolNames ?? "", + SetupNotes = dto.FormProgram.SetupNotes ?? "" + }; + } + + _db.BomItems.Add(item); + await _db.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item)); + } + + private static BomItemDto MapToDto(BomItem b) => new() + { + ID = b.ID, + ItemNo = b.ItemNo, + PartNo = b.PartNo, + SortOrder = b.SortOrder, + Qty = b.Qty, + TotalQty = b.TotalQty, + Description = b.Description, + PartName = b.PartName, + ConfigurationName = b.ConfigurationName, + Material = b.Material, + CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto + { + Id = b.CutTemplate.Id, + DxfFilePath = b.CutTemplate.DxfFilePath, + ContentHash = b.CutTemplate.ContentHash, + Thickness = b.CutTemplate.Thickness, + KFactor = b.CutTemplate.KFactor, + DefaultBendRadius = b.CutTemplate.DefaultBendRadius + }, + FormProgram = b.FormProgram == null ? null : new FormProgramDto + { + Id = b.FormProgram.Id, + ProgramFilePath = b.FormProgram.ProgramFilePath, + ContentHash = b.FormProgram.ContentHash, + ProgramName = b.FormProgram.ProgramName, + Thickness = b.FormProgram.Thickness, + MaterialType = b.FormProgram.MaterialType, + KFactor = b.FormProgram.KFactor, + BendCount = b.FormProgram.BendCount, + UpperToolNames = b.FormProgram.UpperToolNames, + LowerToolNames = b.FormProgram.LowerToolNames, + SetupNotes = b.FormProgram.SetupNotes + } + }; + } +} +``` + +**Step 4: Create FormProgramsController** + +`FabWorks.Api/Controllers/FormProgramsController.cs`: +```csharp +using FabWorks.Api.DTOs; +using FabWorks.Api.Services; +using FabWorks.Core.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/form-programs")] + public class FormProgramsController : ControllerBase + { + private readonly FabWorksDbContext _db; + private readonly FormProgramService _formService; + + public FormProgramsController(FabWorksDbContext db, FormProgramService formService) + { + _db = db; + _formService = formService; + } + + [HttpGet("by-drawing")] + public async Task>> GetByDrawing([FromQuery] string drawingNumber) + { + var programs = await _db.FormPrograms + .Include(fp => fp.BomItem) + .ThenInclude(b => b.ExportRecord) + .Where(fp => fp.BomItem.ExportRecord.DrawingNumber == drawingNumber) + .ToListAsync(); + + return programs.Select(fp => new FormProgramDto + { + Id = fp.Id, + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }).ToList(); + } + + [HttpPost("parse")] + public ActionResult Parse([FromQuery] string filePath) + { + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var fp = _formService.ParseFromFile(filePath); + return new FormProgramDto + { + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }; + } + + [HttpPost("{bomItemId}")] + public async Task> AttachToItem(int bomItemId, [FromQuery] string filePath) + { + var bomItem = await _db.BomItems + .Include(b => b.FormProgram) + .FirstOrDefaultAsync(b => b.ID == bomItemId); + + if (bomItem == null) return NotFound("BOM item not found"); + + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var fp = _formService.ParseFromFile(filePath); + fp.BomItemId = bomItemId; + + // Replace existing if present + if (bomItem.FormProgram != null) + _db.FormPrograms.Remove(bomItem.FormProgram); + + bomItem.FormProgram = fp; + await _db.SaveChangesAsync(); + + return new FormProgramDto + { + Id = fp.Id, + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }; + } + } +} +``` + +**Step 5: Build** + +```bash +dotnet build FabWorks.Api/FabWorks.Api.csproj +``` + +**Step 6: Commit** + +```bash +git add FabWorks.Api/ +git commit -m "feat: add BomItems and FormPrograms controllers with parse service" +``` + +--- + +## Task 6: Add EF Core Migration for FormPrograms Table + +**Step 1: Add migration from FabWorks.Api** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Api +dotnet ef migrations add AddFormPrograms --project ../FabWorks.Core/FabWorks.Core.csproj --startup-project . +``` + +Note: If the migration tooling complains about the existing tables, we may need a baseline migration. The existing ExportDxfDb already has ExportRecords, BomItems, CutTemplates tables. The migration should only add the FormPrograms table and the FK on BomItems. + +**Step 2: Apply migration** + +```bash +dotnet ef database update --project ../FabWorks.Core/FabWorks.Core.csproj --startup-project . +``` + +**Step 3: Commit** + +```bash +git add FabWorks.Core/Migrations/ +git commit -m "feat: add EF migration for FormPrograms table" +``` + +--- + +## Task 7: Add FormProgramService Tests + +**Files:** +- Create: `FabWorks.Tests/FormProgramServiceTests.cs` + +**Step 1: Write tests** + +`FabWorks.Tests/FormProgramServiceTests.cs`: +```csharp +using FabWorks.Api.Services; +using Xunit; + +namespace FabWorks.Tests +{ + public class FormProgramServiceTests + { + [Fact] + public void ParseFromFile_SamplePgm_PopulatesProgramName() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.False(string.IsNullOrEmpty(fp.ProgramName)); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesThickness() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.NotNull(fp.Thickness); + Assert.True(fp.Thickness > 0); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesBendCount() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.True(fp.BendCount > 0); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesToolNames() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.False(string.IsNullOrEmpty(fp.UpperToolNames)); + Assert.False(string.IsNullOrEmpty(fp.LowerToolNames)); + } + + [Fact] + public void ParseFromFile_SamplePgm_ComputesContentHash() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.NotNull(fp.ContentHash); + Assert.Equal(64, fp.ContentHash.Length); // SHA256 hex = 64 chars + } + } +} +``` + +**Step 2: Add project reference to FabWorks.Api** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Tests +dotnet add reference ../FabWorks.Api/FabWorks.Api.csproj +``` + +**Step 3: Run all tests** + +```bash +dotnet test FabWorks.Tests/FabWorks.Tests.csproj -v normal +``` + +Expected: All 10 tests PASS. + +**Step 4: Commit** + +```bash +git add FabWorks.Tests/ +git commit -m "test: add FormProgramService tests" +``` + +--- + +## Task 8: Smoke Test — Run API and Hit Endpoints + +**Step 1: Run the API** + +```bash +cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Api +dotnet run +``` + +**Step 2: Test endpoints with curl/Invoke-WebRequest** + +```powershell +# Create export record +Invoke-RestMethod -Uri "http://localhost:5000/api/exports" -Method Post -ContentType "application/json" -Body '{"drawingNumber":"4980 A05-1","sourceFilePath":"C:\\test.slddrw","outputFolder":"C:\\output"}' + +# Get by ID +Invoke-RestMethod -Uri "http://localhost:5000/api/exports/1" + +# Lookup by source file +Invoke-RestMethod -Uri "http://localhost:5000/api/exports/by-source?path=C:\test.slddrw" + +# Parse a .pgm file +Invoke-RestMethod -Uri "http://localhost:5000/api/form-programs/parse?filePath=S:\4980 GMCH Lockport\4980 A05 Degreaser Casing\4980 A05-1 Access door\Forms\4980 A05-1 PT02.pgm" +``` + +**Step 3: Verify JSON responses look correct** + +**Step 4: Commit any fixes needed** + +--- + +## Future Tasks (Not In This Plan) + +These are follow-up items after the API is proven out: + +1. **Wire ExportDXF to call FabWorks.Api** — Add `IFabWorksClient` interface, replace `_dbContextFactory` usage in `DxfExportService` with HTTP calls +2. **Deploy as Windows Service** — Add `Microsoft.Extensions.Hosting.WindowsServices`, create `deploy.ps1` per deploy script guide +3. **Add Swagger/OpenAPI** — For discoverability +4. **LaserQuote integration** — Point LaserQuote at FabWorks.Api for BOM/material lookups +5. **Batch form program scanner** — Endpoint to scan a directory tree and auto-link .pgm files to BOM items by naming convention