Files
CutList/CutList.Web/Controllers/SeedController.cs
AJ Isaacs f04bf02c42 feat: Migrate MaterialDimensions from TPH to TPC and add Alro catalog seeding
Switch MaterialDimensions inheritance from TPH (single table with discriminator)
to TPC (table per concrete type) with individual tables per shape. Add Swagger
for dev API exploration, expand SeedController with export/import endpoints and
Alro catalog JSON dataset, and include Python scraper for Alro catalog PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:23:01 -05:00

278 lines
11 KiB
C#

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<ActionResult<SeedExportData>> 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<ActionResult> 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<string, Supplier>();
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<MaterialShape>(dto.Shape, out var shape))
{
materialsSkipped++;
continue;
}
Enum.TryParse<MaterialType>(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
};
}