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>
This commit is contained in:
277
CutList.Web/Controllers/SeedController.cs
Normal file
277
CutList.Web/Controllers/SeedController.cs
Normal file
@@ -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<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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user