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:
156
CutList.Web/Controllers/PackingController.cs
Normal file
156
CutList.Web/Controllers/PackingController.cs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user