feat: Add full REST API with controllers, DTOs, and service layer

Add controllers for suppliers, stock items, jobs, cutting tools, and
packing. Refactor MaterialsController to use MaterialService with
dimension-aware CRUD, search, and bulk operations. Extract DTOs into
dedicated files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 16:53:53 -05:00
parent 21d50e7c20
commit 17f16901ef
13 changed files with 1919 additions and 87 deletions

View File

@@ -0,0 +1,156 @@
using CutList.Core;
using CutList.Core.Formatting;
using CutList.Core.Nesting;
using CutList.Web.DTOs;
using Microsoft.AspNetCore.Mvc;
namespace CutList.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class PackingController : ControllerBase
{
[HttpPost("optimize")]
public ActionResult<object> Optimize(StandalonePackRequestDto dto)
{
if (dto.Parts.Count == 0)
return BadRequest("At least one part is required");
if (dto.StockBins.Count == 0)
return BadRequest("At least one stock bin is required");
// Parse parts
var items = new List<BinItem>();
foreach (var part in dto.Parts)
{
double length;
try
{
length = ArchUnits.ParseToInches(part.Length);
}
catch
{
return BadRequest($"Invalid length format for part '{part.Name}': {part.Length}");
}
for (int i = 0; i < part.Quantity; i++)
{
items.Add(new BinItem(part.Name, length));
}
}
// Parse stock bins
var multiBins = new List<MultiBin>();
foreach (var bin in dto.StockBins)
{
double length;
try
{
length = ArchUnits.ParseToInches(bin.Length);
}
catch
{
return BadRequest($"Invalid length format for stock bin: {bin.Length}");
}
multiBins.Add(new MultiBin(length, bin.Quantity, bin.Priority));
}
// Select strategy
var strategy = dto.Strategy?.ToLowerInvariant() switch
{
"bestfit" => PackingStrategy.BestFit,
"exhaustive" => PackingStrategy.Exhaustive,
_ => PackingStrategy.AdvancedFit
};
// Run packing
var engine = new MultiBinEngine
{
Spacing = (double)dto.Kerf,
Strategy = strategy
};
engine.SetBins(multiBins);
var result = engine.Pack(items);
// Map result
var bins = result.Bins.Select(bin => new PackedBinDto
{
LengthInches = bin.Length,
LengthFormatted = ArchUnits.FormatFromInches(bin.Length),
UsedInches = bin.UsedLength,
UsedFormatted = ArchUnits.FormatFromInches(bin.UsedLength),
WasteInches = bin.RemainingLength,
WasteFormatted = ArchUnits.FormatFromInches(bin.RemainingLength),
Efficiency = bin.Length > 0 ? bin.UsedLength / bin.Length * 100 : 0,
Items = bin.Items.Select(i => new PackedItemDto
{
Name = i.Name,
LengthInches = i.Length,
LengthFormatted = ArchUnits.FormatFromInches(i.Length)
}).ToList()
}).ToList();
var itemsNotPlaced = result.ItemsNotUsed.Select(i => new PackedItemDto
{
Name = i.Name,
LengthInches = i.Length,
LengthFormatted = ArchUnits.FormatFromInches(i.Length)
}).ToList();
var totalMaterial = result.Bins.Sum(b => b.Length);
var totalUsed = result.Bins.Sum(b => b.UsedLength);
var totalWaste = result.Bins.Sum(b => b.RemainingLength);
return Ok(new
{
Bins = bins,
ItemsNotPlaced = itemsNotPlaced,
Summary = new
{
TotalBins = result.Bins.Count,
TotalPieces = result.Bins.Sum(b => b.Items.Count),
TotalMaterialInches = totalMaterial,
TotalMaterialFormatted = ArchUnits.FormatFromInches(totalMaterial),
TotalUsedInches = totalUsed,
TotalUsedFormatted = ArchUnits.FormatFromInches(totalUsed),
TotalWasteInches = totalWaste,
TotalWasteFormatted = ArchUnits.FormatFromInches(totalWaste),
Efficiency = totalMaterial > 0 ? totalUsed / totalMaterial * 100 : 0,
ItemsNotPlaced = result.ItemsNotUsed.Count
}
});
}
[HttpPost("parse-length")]
public ActionResult<ParseLengthResponseDto> ParseLength(ParseLengthRequestDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Input))
return BadRequest("Input is required");
try
{
var inches = ArchUnits.ParseToInches(dto.Input);
return Ok(new ParseLengthResponseDto
{
Inches = inches,
Formatted = ArchUnits.FormatFromInches(inches)
});
}
catch (Exception ex)
{
return BadRequest($"Could not parse '{dto.Input}': {ex.Message}");
}
}
[HttpPost("format-length")]
public ActionResult<FormatLengthResponseDto> FormatLength(FormatLengthRequestDto dto)
{
return Ok(new FormatLengthResponseDto
{
Inches = dto.Inches,
Formatted = ArchUnits.FormatFromInches(dto.Inches)
});
}
}