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:
97
CutList.Web/Controllers/CuttingToolsController.cs
Normal file
97
CutList.Web/Controllers/CuttingToolsController.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/cutting-tools")]
|
||||
public class CuttingToolsController : ControllerBase
|
||||
{
|
||||
private readonly JobService _jobService;
|
||||
|
||||
public CuttingToolsController(JobService jobService)
|
||||
{
|
||||
_jobService = jobService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<CuttingToolDto>>> GetAll([FromQuery] bool includeInactive = false)
|
||||
{
|
||||
var tools = await _jobService.GetCuttingToolsAsync(includeInactive);
|
||||
return Ok(tools.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<CuttingToolDto>> GetById(int id)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpGet("default")]
|
||||
public async Task<ActionResult<CuttingToolDto>> GetDefault()
|
||||
{
|
||||
var tool = await _jobService.GetDefaultCuttingToolAsync();
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CuttingToolDto>> Create(CreateCuttingToolDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
return BadRequest("Name is required");
|
||||
|
||||
var tool = new CuttingTool
|
||||
{
|
||||
Name = dto.Name,
|
||||
KerfInches = dto.KerfInches,
|
||||
IsDefault = dto.IsDefault
|
||||
};
|
||||
|
||||
await _jobService.CreateCuttingToolAsync(tool);
|
||||
return CreatedAtAction(nameof(GetById), new { id = tool.Id }, MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<CuttingToolDto>> Update(int id, UpdateCuttingToolDto dto)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Name != null) tool.Name = dto.Name;
|
||||
if (dto.KerfInches.HasValue) tool.KerfInches = dto.KerfInches.Value;
|
||||
if (dto.IsDefault.HasValue) tool.IsDefault = dto.IsDefault.Value;
|
||||
|
||||
await _jobService.UpdateCuttingToolAsync(tool);
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteCuttingToolAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static CuttingToolDto MapToDto(CuttingTool tool) => new()
|
||||
{
|
||||
Id = tool.Id,
|
||||
Name = tool.Name,
|
||||
KerfInches = tool.KerfInches,
|
||||
IsDefault = tool.IsDefault,
|
||||
IsActive = tool.IsActive
|
||||
};
|
||||
}
|
||||
492
CutList.Web/Controllers/JobsController.cs
Normal file
492
CutList.Web/Controllers/JobsController.cs
Normal file
@@ -0,0 +1,492 @@
|
||||
using CutList.Core.Formatting;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JobsController : ControllerBase
|
||||
{
|
||||
private readonly JobService _jobService;
|
||||
private readonly CutListPackingService _packingService;
|
||||
|
||||
public JobsController(JobService jobService, CutListPackingService packingService)
|
||||
{
|
||||
_jobService = jobService;
|
||||
_packingService = packingService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<JobDto>>> GetAll()
|
||||
{
|
||||
var jobs = await _jobService.GetAllAsync();
|
||||
return Ok(jobs.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<JobDetailDto>> GetById(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDetailDto(job));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<JobDetailDto>> Create(CreateJobDto dto)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
Name = dto.Name,
|
||||
Customer = dto.Customer,
|
||||
CuttingToolId = dto.CuttingToolId,
|
||||
Notes = dto.Notes
|
||||
};
|
||||
|
||||
await _jobService.CreateAsync(job);
|
||||
var created = await _jobService.GetByIdAsync(job.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = job.Id }, MapToDetailDto(created!));
|
||||
}
|
||||
|
||||
[HttpPost("quick-create")]
|
||||
public async Task<ActionResult<JobDetailDto>> QuickCreate(QuickCreateJobDto dto)
|
||||
{
|
||||
var job = await _jobService.QuickCreateAsync(dto.Customer);
|
||||
var created = await _jobService.GetByIdAsync(job.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = job.Id }, MapToDetailDto(created!));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<JobDetailDto>> Update(int id, UpdateJobDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Name != null) job.Name = dto.Name;
|
||||
if (dto.Customer != null) job.Customer = dto.Customer;
|
||||
if (dto.CuttingToolId.HasValue) job.CuttingToolId = dto.CuttingToolId;
|
||||
if (dto.Notes != null) job.Notes = dto.Notes;
|
||||
|
||||
await _jobService.UpdateAsync(job);
|
||||
|
||||
var updated = await _jobService.GetByIdAsync(id);
|
||||
return Ok(MapToDetailDto(updated!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/duplicate")]
|
||||
public async Task<ActionResult<JobDetailDto>> Duplicate(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var duplicate = await _jobService.DuplicateAsync(id);
|
||||
var loaded = await _jobService.GetByIdAsync(duplicate.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = duplicate.Id }, MapToDetailDto(loaded!));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parts ---
|
||||
|
||||
[HttpGet("{id}/parts")]
|
||||
public async Task<ActionResult<List<JobPartDto>>> GetParts(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(job.Parts.Select(MapPartToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/parts")]
|
||||
public async Task<ActionResult<JobPartDto>> AddPart(int id, CreateJobPartDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
decimal lengthInches;
|
||||
try
|
||||
{
|
||||
lengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
|
||||
var part = new JobPart
|
||||
{
|
||||
JobId = id,
|
||||
MaterialId = dto.MaterialId,
|
||||
Name = dto.Name,
|
||||
LengthInches = lengthInches,
|
||||
Quantity = dto.Quantity
|
||||
};
|
||||
|
||||
await _jobService.AddPartAsync(part);
|
||||
|
||||
// Reload to get material name
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var addedPart = reloadedJob!.Parts.FirstOrDefault(p => p.Id == part.Id);
|
||||
return CreatedAtAction(nameof(GetParts), new { id }, MapPartToDto(addedPart ?? part));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/parts/{partId}")]
|
||||
public async Task<ActionResult<JobPartDto>> UpdatePart(int id, int partId, UpdateJobPartDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var part = job.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
if (part == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.MaterialId.HasValue) part.MaterialId = dto.MaterialId.Value;
|
||||
if (dto.Name != null) part.Name = dto.Name;
|
||||
if (dto.Length != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
part.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
}
|
||||
if (dto.Quantity.HasValue) part.Quantity = dto.Quantity.Value;
|
||||
|
||||
await _jobService.UpdatePartAsync(part);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var updatedPart = reloadedJob!.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
return Ok(MapPartToDto(updatedPart ?? part));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/parts/{partId}")]
|
||||
public async Task<IActionResult> DeletePart(int id, int partId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var part = job.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
if (part == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeletePartAsync(partId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// --- Stock ---
|
||||
|
||||
[HttpGet("{id}/stock")]
|
||||
public async Task<ActionResult<List<JobStockDto>>> GetStock(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(job.Stock.Select(MapStockToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/stock")]
|
||||
public async Task<ActionResult<JobStockDto>> AddStock(int id, CreateJobStockDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
decimal lengthInches;
|
||||
try
|
||||
{
|
||||
lengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
|
||||
var stock = new JobStock
|
||||
{
|
||||
JobId = id,
|
||||
MaterialId = dto.MaterialId,
|
||||
StockItemId = dto.StockItemId,
|
||||
LengthInches = lengthInches,
|
||||
Quantity = dto.Quantity,
|
||||
IsCustomLength = dto.IsCustomLength,
|
||||
Priority = dto.Priority
|
||||
};
|
||||
|
||||
await _jobService.AddStockAsync(stock);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var addedStock = reloadedJob!.Stock.FirstOrDefault(s => s.Id == stock.Id);
|
||||
return CreatedAtAction(nameof(GetStock), new { id }, MapStockToDto(addedStock ?? stock));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/stock/{stockId}")]
|
||||
public async Task<ActionResult<JobStockDto>> UpdateStock(int id, int stockId, UpdateJobStockDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var stock = job.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
if (stock == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.StockItemId.HasValue) stock.StockItemId = dto.StockItemId;
|
||||
if (dto.Length != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
stock.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
}
|
||||
if (dto.Quantity.HasValue) stock.Quantity = dto.Quantity.Value;
|
||||
if (dto.IsCustomLength.HasValue) stock.IsCustomLength = dto.IsCustomLength.Value;
|
||||
if (dto.Priority.HasValue) stock.Priority = dto.Priority.Value;
|
||||
|
||||
await _jobService.UpdateStockAsync(stock);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var updatedStock = reloadedJob!.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
return Ok(MapStockToDto(updatedStock ?? stock));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/stock/{stockId}")]
|
||||
public async Task<IActionResult> DeleteStock(int id, int stockId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var stock = job.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
if (stock == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteStockAsync(stockId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/available-stock/{materialId}")]
|
||||
public async Task<ActionResult<List<StockItemDto>>> GetAvailableStock(int id, int materialId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var items = await _jobService.GetAvailableStockForMaterialAsync(materialId);
|
||||
return Ok(items.Select(s => new StockItemDto
|
||||
{
|
||||
Id = s.Id,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.Material?.DisplayName ?? string.Empty,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||
Name = s.Name,
|
||||
QuantityOnHand = s.QuantityOnHand,
|
||||
IsActive = s.IsActive
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
// --- Packing ---
|
||||
|
||||
[HttpPost("{id}/pack")]
|
||||
public async Task<ActionResult<PackResponseDto>> Pack(int id, PackJobRequestDto? dto = null)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
if (job.Parts.Count == 0)
|
||||
return BadRequest("Job has no parts to pack");
|
||||
|
||||
// Determine kerf
|
||||
decimal kerf = dto?.KerfOverride
|
||||
?? job.CuttingTool?.KerfInches
|
||||
?? (await _jobService.GetDefaultCuttingToolAsync())?.KerfInches
|
||||
?? 0.125m;
|
||||
|
||||
var result = await _packingService.PackAsync(job.Parts, kerf, job.Stock.Any() ? job.Stock : null);
|
||||
var summary = _packingService.GetSummary(result);
|
||||
|
||||
return Ok(MapPackResult(result, summary));
|
||||
}
|
||||
|
||||
// --- Mapping helpers ---
|
||||
|
||||
private static JobDto MapToDto(Job j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingTool?.Name,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.Parts?.Count ?? 0,
|
||||
StockCount = j.Stock?.Count ?? 0
|
||||
};
|
||||
|
||||
private static JobDetailDto MapToDetailDto(Job j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingTool?.Name,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.Parts?.Count ?? 0,
|
||||
StockCount = j.Stock?.Count ?? 0,
|
||||
Parts = j.Parts?.Select(MapPartToDto).ToList() ?? new(),
|
||||
Stock = j.Stock?.Select(MapStockToDto).ToList() ?? new()
|
||||
};
|
||||
|
||||
private static JobPartDto MapPartToDto(JobPart p) => new()
|
||||
{
|
||||
Id = p.Id,
|
||||
JobId = p.JobId,
|
||||
MaterialId = p.MaterialId,
|
||||
MaterialName = p.Material?.DisplayName ?? string.Empty,
|
||||
Name = p.Name,
|
||||
LengthInches = p.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)p.LengthInches),
|
||||
Quantity = p.Quantity,
|
||||
SortOrder = p.SortOrder
|
||||
};
|
||||
|
||||
private static JobStockDto MapStockToDto(JobStock s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
JobId = s.JobId,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.Material?.DisplayName ?? string.Empty,
|
||||
StockItemId = s.StockItemId,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||
Quantity = s.Quantity,
|
||||
IsCustomLength = s.IsCustomLength,
|
||||
Priority = s.Priority,
|
||||
SortOrder = s.SortOrder
|
||||
};
|
||||
|
||||
private static PackResponseDto MapPackResult(MultiMaterialPackResult result, MultiMaterialPackingSummary summary)
|
||||
{
|
||||
var response = new PackResponseDto();
|
||||
|
||||
foreach (var mr in result.MaterialResults)
|
||||
{
|
||||
var matResult = new MaterialPackResultDto
|
||||
{
|
||||
MaterialId = mr.Material.Id,
|
||||
MaterialName = mr.Material.DisplayName,
|
||||
InStockBins = mr.InStockBins.Select(MapBinToDto).ToList(),
|
||||
ToBePurchasedBins = mr.ToBePurchasedBins.Select(MapBinToDto).ToList(),
|
||||
ItemsNotPlaced = mr.PackResult.ItemsNotUsed.Select(i => new PackedItemDto
|
||||
{
|
||||
Name = i.Name,
|
||||
LengthInches = i.Length,
|
||||
LengthFormatted = ArchUnits.FormatFromInches(i.Length)
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var ms = summary.MaterialSummaries.FirstOrDefault(s => s.Material.Id == mr.Material.Id);
|
||||
if (ms != null)
|
||||
{
|
||||
matResult.Summary = new MaterialPackingSummaryDto
|
||||
{
|
||||
MaterialId = ms.Material.Id,
|
||||
MaterialName = ms.Material.DisplayName,
|
||||
InStockBins = ms.InStockBins,
|
||||
ToBePurchasedBins = ms.ToBePurchasedBins,
|
||||
TotalPieces = ms.TotalPieces,
|
||||
TotalMaterialInches = ms.TotalMaterial,
|
||||
TotalUsedInches = ms.TotalUsed,
|
||||
TotalWasteInches = ms.TotalWaste,
|
||||
Efficiency = ms.Efficiency,
|
||||
ItemsNotPlaced = ms.ItemsNotPlaced
|
||||
};
|
||||
}
|
||||
|
||||
response.Materials.Add(matResult);
|
||||
}
|
||||
|
||||
response.Summary = new PackingSummaryDto
|
||||
{
|
||||
TotalInStockBins = summary.TotalInStockBins,
|
||||
TotalToBePurchasedBins = summary.TotalToBePurchasedBins,
|
||||
TotalPieces = summary.TotalPieces,
|
||||
TotalMaterialInches = summary.TotalMaterial,
|
||||
TotalMaterialFormatted = ArchUnits.FormatFromInches(summary.TotalMaterial),
|
||||
TotalUsedInches = summary.TotalUsed,
|
||||
TotalUsedFormatted = ArchUnits.FormatFromInches(summary.TotalUsed),
|
||||
TotalWasteInches = summary.TotalWaste,
|
||||
TotalWasteFormatted = ArchUnits.FormatFromInches(summary.TotalWaste),
|
||||
Efficiency = summary.Efficiency,
|
||||
TotalItemsNotPlaced = summary.TotalItemsNotPlaced,
|
||||
MaterialSummaries = summary.MaterialSummaries.Select(ms => new MaterialPackingSummaryDto
|
||||
{
|
||||
MaterialId = ms.Material.Id,
|
||||
MaterialName = ms.Material.DisplayName,
|
||||
InStockBins = ms.InStockBins,
|
||||
ToBePurchasedBins = ms.ToBePurchasedBins,
|
||||
TotalPieces = ms.TotalPieces,
|
||||
TotalMaterialInches = ms.TotalMaterial,
|
||||
TotalUsedInches = ms.TotalUsed,
|
||||
TotalWasteInches = ms.TotalWaste,
|
||||
Efficiency = ms.Efficiency,
|
||||
ItemsNotPlaced = ms.ItemsNotPlaced
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static PackedBinDto MapBinToDto(CutList.Core.Bin bin) => new()
|
||||
{
|
||||
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()
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
@@ -9,48 +9,55 @@ namespace CutList.Web.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
public class MaterialsController : ControllerBase
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly MaterialService _materialService;
|
||||
|
||||
public MaterialsController(ApplicationDbContext context)
|
||||
public MaterialsController(MaterialService materialService)
|
||||
{
|
||||
_context = context;
|
||||
_materialService = materialService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MaterialDto>>> GetMaterials()
|
||||
public async Task<ActionResult<List<MaterialDto>>> GetMaterials(
|
||||
[FromQuery] bool includeInactive = false,
|
||||
[FromQuery] string? shape = null)
|
||||
{
|
||||
var materials = await _context.Materials
|
||||
.Where(m => m.IsActive)
|
||||
.OrderBy(m => m.Shape)
|
||||
.ThenBy(m => m.SortOrder)
|
||||
.ThenBy(m => m.Size)
|
||||
.Select(m => new MaterialDto
|
||||
{
|
||||
Id = m.Id,
|
||||
Shape = m.Shape.GetDisplayName(),
|
||||
Size = m.Size,
|
||||
Description = m.Description
|
||||
})
|
||||
.ToListAsync();
|
||||
List<Material> materials;
|
||||
|
||||
return Ok(materials);
|
||||
if (!string.IsNullOrWhiteSpace(shape))
|
||||
{
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {shape}");
|
||||
|
||||
materials = await _materialService.GetByShapeAsync(parsedShape.Value, includeInactive);
|
||||
}
|
||||
else
|
||||
{
|
||||
materials = await _materialService.GetAllAsync(includeInactive);
|
||||
}
|
||||
|
||||
return Ok(materials.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<MaterialDto>> GetMaterial(int id)
|
||||
{
|
||||
var material = await _context.Materials.FindAsync(id);
|
||||
|
||||
if (material == null || !material.IsActive)
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(new MaterialDto
|
||||
{
|
||||
Id = material.Id,
|
||||
Shape = material.Shape.GetDisplayName(),
|
||||
Size = material.Size,
|
||||
Description = material.Description
|
||||
});
|
||||
return Ok(MapToDto(material));
|
||||
}
|
||||
|
||||
[HttpGet("by-shape/{shape}")]
|
||||
public async Task<ActionResult<List<MaterialDto>>> GetByShape(string shape, [FromQuery] bool includeInactive = false)
|
||||
{
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {shape}");
|
||||
|
||||
var materials = await _materialService.GetByShapeAsync(parsedShape.Value, includeInactive);
|
||||
return Ok(materials.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -59,38 +66,53 @@ public class MaterialsController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
return BadRequest("Shape is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.Size))
|
||||
return BadRequest("Size is required");
|
||||
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(dto.Shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {dto.Shape}");
|
||||
|
||||
// Check for duplicates
|
||||
var exists = await _context.Materials
|
||||
.AnyAsync(m => m.Shape == parsedShape.Value && m.Size == dto.Size && m.IsActive);
|
||||
|
||||
if (exists)
|
||||
return Conflict($"Material '{dto.Shape} - {dto.Size}' already exists");
|
||||
// Parse material type
|
||||
MaterialType materialType = MaterialType.Steel;
|
||||
if (!string.IsNullOrWhiteSpace(dto.Type))
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out materialType))
|
||||
return BadRequest($"Unknown material type: {dto.Type}");
|
||||
}
|
||||
|
||||
var material = new Material
|
||||
{
|
||||
Shape = parsedShape.Value,
|
||||
Size = dto.Size,
|
||||
Description = dto.Description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
Type = materialType,
|
||||
Grade = dto.Grade,
|
||||
Size = dto.Size ?? string.Empty,
|
||||
Description = dto.Description
|
||||
};
|
||||
|
||||
_context.Materials.Add(material);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = material.Id }, new MaterialDto
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
Id = material.Id,
|
||||
Shape = material.Shape.GetDisplayName(),
|
||||
Size = material.Size,
|
||||
Description = material.Description
|
||||
});
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
|
||||
// Check for duplicates using generated size
|
||||
var generatedSize = dimensions.GenerateSizeString();
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, generatedSize);
|
||||
if (exists)
|
||||
return Conflict($"Material '{parsedShape.Value.GetDisplayName()} - {generatedSize}' already exists");
|
||||
|
||||
var created = await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(material.Size))
|
||||
return BadRequest("Size is required when dimensions are not provided");
|
||||
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, material.Size);
|
||||
if (exists)
|
||||
return Conflict($"Material '{parsedShape.Value.GetDisplayName()} - {material.Size}' already exists");
|
||||
|
||||
var created = await _materialService.CreateAsync(material);
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bulk")]
|
||||
@@ -102,9 +124,9 @@ public class MaterialsController : ControllerBase
|
||||
|
||||
foreach (var dto in materials)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape) || string.IsNullOrWhiteSpace(dto.Size))
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
{
|
||||
errors.Add($"Invalid material: Shape and Size are required");
|
||||
errors.Add("Invalid material: Shape is required");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -115,27 +137,61 @@ public class MaterialsController : ControllerBase
|
||||
continue;
|
||||
}
|
||||
|
||||
var exists = await _context.Materials
|
||||
.AnyAsync(m => m.Shape == parsedShape.Value && m.Size == dto.Size && m.IsActive);
|
||||
var size = dto.Size ?? string.Empty;
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
size = dimensions.GenerateSizeString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(size))
|
||||
{
|
||||
errors.Add($"Size is required for {dto.Shape}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, size);
|
||||
if (exists)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_context.Materials.Add(new Material
|
||||
MaterialType materialType = MaterialType.Steel;
|
||||
if (!string.IsNullOrWhiteSpace(dto.Type))
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out materialType))
|
||||
{
|
||||
errors.Add($"Unknown material type: {dto.Type}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var material = new Material
|
||||
{
|
||||
Shape = parsedShape.Value,
|
||||
Size = dto.Size,
|
||||
Description = dto.Description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
Type = materialType,
|
||||
Grade = dto.Grade,
|
||||
Size = size,
|
||||
Description = dto.Description
|
||||
};
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _materialService.CreateAsync(material);
|
||||
}
|
||||
|
||||
created++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new BulkCreateResult
|
||||
{
|
||||
Created = created,
|
||||
@@ -144,39 +200,192 @@ public class MaterialsController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteMaterial(int id)
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<MaterialDto>> UpdateMaterial(int id, UpdateMaterialDto dto)
|
||||
{
|
||||
var material = await _context.Materials.FindAsync(id);
|
||||
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
material.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
if (dto.Type != null)
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out var materialType))
|
||||
return BadRequest($"Unknown material type: {dto.Type}");
|
||||
material.Type = materialType;
|
||||
}
|
||||
|
||||
if (dto.Grade != null) material.Grade = dto.Grade;
|
||||
if (dto.Size != null) material.Size = dto.Size;
|
||||
if (dto.Description != null) material.Description = dto.Description;
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = material.Dimensions ?? MaterialService.CreateDimensionsForShape(material.Shape);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
await _materialService.UpdateWithDimensionsAsync(material, dimensions, dto.RegenerateSize ?? false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _materialService.UpdateAsync(material);
|
||||
}
|
||||
|
||||
var updated = await _materialService.GetByIdAsync(id);
|
||||
return Ok(MapToDto(updated!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteMaterial(int id)
|
||||
{
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
await _materialService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public class MaterialDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
[HttpPost("search")]
|
||||
public async Task<ActionResult<List<MaterialDto>>> SearchMaterials(MaterialSearchDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
return BadRequest("Shape is required");
|
||||
|
||||
public class CreateMaterialDto
|
||||
{
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(dto.Shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {dto.Shape}");
|
||||
|
||||
public class BulkCreateResult
|
||||
{
|
||||
public int Created { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
var results = parsedShape.Value switch
|
||||
{
|
||||
MaterialShape.RoundBar => await _materialService.SearchRoundBarByDiameterAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.RoundTube => await _materialService.SearchRoundTubeByODAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.FlatBar => await _materialService.SearchFlatBarByWidthAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.SquareBar => await _materialService.SearchSquareBarBySizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.SquareTube => await _materialService.SearchSquareTubeBySizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.RectangularTube => await _materialService.SearchRectangularTubeByWidthAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Angle => await _materialService.SearchAngleByLegAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Channel => await _materialService.SearchChannelByHeightAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.IBeam => await _materialService.SearchIBeamByHeightAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Pipe => await _materialService.SearchPipeByNominalSizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
_ => new List<Material>()
|
||||
};
|
||||
|
||||
return Ok(results.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
private static MaterialDto MapToDto(Material m) => new()
|
||||
{
|
||||
Id = m.Id,
|
||||
Shape = m.Shape.GetDisplayName(),
|
||||
Type = m.Type.ToString(),
|
||||
Grade = m.Grade,
|
||||
Size = m.Size,
|
||||
Description = m.Description,
|
||||
IsActive = m.IsActive,
|
||||
Dimensions = m.Dimensions != null ? MapDimensionsToDto(m.Dimensions) : null
|
||||
};
|
||||
|
||||
private static MaterialDimensionsDto MapDimensionsToDto(MaterialDimensions d)
|
||||
{
|
||||
var dto = new MaterialDimensionsDto
|
||||
{
|
||||
DimensionType = d.GetType().Name.Replace("Dimensions", "")
|
||||
};
|
||||
|
||||
// Extract dimension values based on type
|
||||
switch (d)
|
||||
{
|
||||
case RoundBarDimensions rb:
|
||||
dto.Values["Diameter"] = rb.Diameter;
|
||||
break;
|
||||
case RoundTubeDimensions rt:
|
||||
dto.Values["OuterDiameter"] = rt.OuterDiameter;
|
||||
dto.Values["Wall"] = rt.Wall;
|
||||
break;
|
||||
case FlatBarDimensions fb:
|
||||
dto.Values["Width"] = fb.Width;
|
||||
dto.Values["Thickness"] = fb.Thickness;
|
||||
break;
|
||||
case SquareBarDimensions sb:
|
||||
dto.Values["Size"] = sb.Size;
|
||||
break;
|
||||
case SquareTubeDimensions st:
|
||||
dto.Values["Size"] = st.Size;
|
||||
dto.Values["Wall"] = st.Wall;
|
||||
break;
|
||||
case RectangularTubeDimensions rect:
|
||||
dto.Values["Width"] = rect.Width;
|
||||
dto.Values["Height"] = rect.Height;
|
||||
dto.Values["Wall"] = rect.Wall;
|
||||
break;
|
||||
case AngleDimensions a:
|
||||
dto.Values["Leg1"] = a.Leg1;
|
||||
dto.Values["Leg2"] = a.Leg2;
|
||||
dto.Values["Thickness"] = a.Thickness;
|
||||
break;
|
||||
case ChannelDimensions c:
|
||||
dto.Values["Height"] = c.Height;
|
||||
dto.Values["Flange"] = c.Flange;
|
||||
dto.Values["Web"] = c.Web;
|
||||
break;
|
||||
case IBeamDimensions ib:
|
||||
dto.Values["Height"] = ib.Height;
|
||||
dto.Values["WeightPerFoot"] = ib.WeightPerFoot;
|
||||
break;
|
||||
case PipeDimensions p:
|
||||
dto.Values["NominalSize"] = p.NominalSize;
|
||||
if (p.Wall.HasValue) dto.Values["Wall"] = p.Wall.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static void ApplyDimensionValues(MaterialDimensions dimensions, Dictionary<string, decimal> values)
|
||||
{
|
||||
switch (dimensions)
|
||||
{
|
||||
case RoundBarDimensions rb:
|
||||
if (values.TryGetValue("Diameter", out var diameter)) rb.Diameter = diameter;
|
||||
break;
|
||||
case RoundTubeDimensions rt:
|
||||
if (values.TryGetValue("OuterDiameter", out var od)) rt.OuterDiameter = od;
|
||||
if (values.TryGetValue("Wall", out var rtWall)) rt.Wall = rtWall;
|
||||
break;
|
||||
case FlatBarDimensions fb:
|
||||
if (values.TryGetValue("Width", out var fbWidth)) fb.Width = fbWidth;
|
||||
if (values.TryGetValue("Thickness", out var fbThick)) fb.Thickness = fbThick;
|
||||
break;
|
||||
case SquareBarDimensions sb:
|
||||
if (values.TryGetValue("Size", out var sbSize)) sb.Size = sbSize;
|
||||
break;
|
||||
case SquareTubeDimensions st:
|
||||
if (values.TryGetValue("Size", out var stSize)) st.Size = stSize;
|
||||
if (values.TryGetValue("Wall", out var stWall)) st.Wall = stWall;
|
||||
break;
|
||||
case RectangularTubeDimensions rect:
|
||||
if (values.TryGetValue("Width", out var rectWidth)) rect.Width = rectWidth;
|
||||
if (values.TryGetValue("Height", out var rectHeight)) rect.Height = rectHeight;
|
||||
if (values.TryGetValue("Wall", out var rectWall)) rect.Wall = rectWall;
|
||||
break;
|
||||
case AngleDimensions a:
|
||||
if (values.TryGetValue("Leg1", out var leg1)) a.Leg1 = leg1;
|
||||
if (values.TryGetValue("Leg2", out var leg2)) a.Leg2 = leg2;
|
||||
if (values.TryGetValue("Thickness", out var aThick)) a.Thickness = aThick;
|
||||
break;
|
||||
case ChannelDimensions c:
|
||||
if (values.TryGetValue("Height", out var cHeight)) c.Height = cHeight;
|
||||
if (values.TryGetValue("Flange", out var flange)) c.Flange = flange;
|
||||
if (values.TryGetValue("Web", out var web)) c.Web = web;
|
||||
break;
|
||||
case IBeamDimensions ib:
|
||||
if (values.TryGetValue("Height", out var ibHeight)) ib.Height = ibHeight;
|
||||
if (values.TryGetValue("WeightPerFoot", out var weight)) ib.WeightPerFoot = weight;
|
||||
break;
|
||||
case PipeDimensions p:
|
||||
if (values.TryGetValue("NominalSize", out var nps)) p.NominalSize = nps;
|
||||
if (values.TryGetValue("Wall", out var pWall)) p.Wall = pWall;
|
||||
if (values.TryGetValue("Schedule", out var schedule)) p.Schedule = schedule.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
272
CutList.Web/Controllers/StockItemsController.cs
Normal file
272
CutList.Web/Controllers/StockItemsController.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using CutList.Core.Formatting;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/stock-items")]
|
||||
public class StockItemsController : ControllerBase
|
||||
{
|
||||
private readonly StockItemService _stockItemService;
|
||||
private readonly SupplierService _supplierService;
|
||||
|
||||
public StockItemsController(StockItemService stockItemService, SupplierService supplierService)
|
||||
{
|
||||
_stockItemService = stockItemService;
|
||||
_supplierService = supplierService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<StockItemDto>>> GetAll(
|
||||
[FromQuery] bool includeInactive = false,
|
||||
[FromQuery] int? materialId = null)
|
||||
{
|
||||
List<StockItem> items;
|
||||
if (materialId.HasValue)
|
||||
items = await _stockItemService.GetByMaterialAsync(materialId.Value, includeInactive);
|
||||
else
|
||||
items = await _stockItemService.GetAllAsync(includeInactive);
|
||||
|
||||
return Ok(items.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<StockItemDto>> GetById(int id)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(item));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<StockItemDto>> Create(CreateStockItemDto dto)
|
||||
{
|
||||
double lengthInches;
|
||||
try
|
||||
{
|
||||
lengthInches = ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
|
||||
var exists = await _stockItemService.ExistsAsync(dto.MaterialId, (decimal)lengthInches);
|
||||
if (exists)
|
||||
return Conflict("A stock item with this material and length already exists");
|
||||
|
||||
var stockItem = new StockItem
|
||||
{
|
||||
MaterialId = dto.MaterialId,
|
||||
LengthInches = (decimal)lengthInches,
|
||||
Name = dto.Name,
|
||||
QuantityOnHand = dto.QuantityOnHand,
|
||||
Notes = dto.Notes
|
||||
};
|
||||
|
||||
await _stockItemService.CreateAsync(stockItem);
|
||||
|
||||
// Reload with includes
|
||||
var created = await _stockItemService.GetByIdAsync(stockItem.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = stockItem.Id }, MapToDto(created!));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<StockItemDto>> Update(int id, UpdateStockItemDto dto)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Length != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
item.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.Name != null) item.Name = dto.Name;
|
||||
if (dto.Notes != null) item.Notes = dto.Notes;
|
||||
|
||||
await _stockItemService.UpdateAsync(item);
|
||||
return Ok(MapToDto(item));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
await _stockItemService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("by-material/{materialId}")]
|
||||
public async Task<ActionResult<List<StockItemDto>>> GetByMaterial(int materialId)
|
||||
{
|
||||
var items = await _stockItemService.GetByMaterialAsync(materialId);
|
||||
return Ok(items.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}/offerings")]
|
||||
public async Task<ActionResult<List<OfferingDto>>> GetOfferings(int id)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
var offerings = await _supplierService.GetOfferingsForStockItemAsync(id);
|
||||
return Ok(offerings.Select(MapOfferingToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}/pricing")]
|
||||
public async Task<ActionResult<StockPricingDto>> GetPricing(int id)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
var avgCost = await _stockItemService.GetAverageCostAsync(id);
|
||||
var lastPrice = await _stockItemService.GetLastPurchasePriceAsync(id);
|
||||
|
||||
return Ok(new StockPricingDto
|
||||
{
|
||||
AverageCost = avgCost,
|
||||
LastPurchasePrice = lastPrice
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{id}/transactions")]
|
||||
public async Task<ActionResult<List<StockTransactionDto>>> GetTransactions(int id, [FromQuery] int? limit = null)
|
||||
{
|
||||
var item = await _stockItemService.GetByIdAsync(id);
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
var transactions = await _stockItemService.GetTransactionHistoryAsync(id, limit);
|
||||
return Ok(transactions.Select(MapTransactionToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/receive")]
|
||||
public async Task<ActionResult<StockTransactionDto>> ReceiveStock(int id, AddStockDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transaction = await _stockItemService.AddStockAsync(id, dto.Quantity, dto.SupplierId, dto.UnitPrice, dto.Notes);
|
||||
return Ok(MapTransactionToDto(transaction));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/use")]
|
||||
public async Task<ActionResult<StockTransactionDto>> UseStock(int id, UseStockDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transaction = await _stockItemService.UseStockAsync(id, dto.Quantity, dto.JobId, dto.Notes);
|
||||
return Ok(MapTransactionToDto(transaction));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/adjust")]
|
||||
public async Task<ActionResult<StockTransactionDto>> AdjustStock(int id, AdjustStockDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transaction = await _stockItemService.AdjustStockAsync(id, dto.NewQuantity, dto.Notes);
|
||||
return Ok(MapTransactionToDto(transaction));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/scrap")]
|
||||
public async Task<ActionResult<StockTransactionDto>> ScrapStock(int id, ScrapStockDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transaction = await _stockItemService.ScrapStockAsync(id, dto.Quantity, dto.Notes);
|
||||
return Ok(MapTransactionToDto(transaction));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/recalculate")]
|
||||
public async Task<ActionResult<object>> RecalculateStock(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newQuantity = await _stockItemService.RecalculateQuantityAsync(id);
|
||||
return Ok(new { QuantityOnHand = newQuantity });
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
private static StockItemDto MapToDto(StockItem s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.Material?.DisplayName ?? string.Empty,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||
Name = s.Name,
|
||||
QuantityOnHand = s.QuantityOnHand,
|
||||
Notes = s.Notes,
|
||||
IsActive = s.IsActive
|
||||
};
|
||||
|
||||
private static StockTransactionDto MapTransactionToDto(StockTransaction t) => new()
|
||||
{
|
||||
Id = t.Id,
|
||||
StockItemId = t.StockItemId,
|
||||
Quantity = t.Quantity,
|
||||
Type = t.Type.ToString(),
|
||||
JobId = t.JobId,
|
||||
JobNumber = t.Job?.JobNumber,
|
||||
SupplierId = t.SupplierId,
|
||||
SupplierName = t.Supplier?.Name,
|
||||
UnitPrice = t.UnitPrice,
|
||||
Notes = t.Notes,
|
||||
CreatedAt = t.CreatedAt
|
||||
};
|
||||
|
||||
private static OfferingDto MapOfferingToDto(SupplierOffering o) => new()
|
||||
{
|
||||
Id = o.Id,
|
||||
SupplierId = o.SupplierId,
|
||||
SupplierName = o.Supplier?.Name,
|
||||
StockItemId = o.StockItemId,
|
||||
PartNumber = o.PartNumber,
|
||||
SupplierDescription = o.SupplierDescription,
|
||||
Price = o.Price,
|
||||
Notes = o.Notes,
|
||||
IsActive = o.IsActive
|
||||
};
|
||||
}
|
||||
172
CutList.Web/Controllers/SuppliersController.cs
Normal file
172
CutList.Web/Controllers/SuppliersController.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using CutList.Core.Formatting;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SuppliersController : ControllerBase
|
||||
{
|
||||
private readonly SupplierService _supplierService;
|
||||
|
||||
public SuppliersController(SupplierService supplierService)
|
||||
{
|
||||
_supplierService = supplierService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<SupplierDto>>> GetAll([FromQuery] bool includeInactive = false)
|
||||
{
|
||||
var suppliers = await _supplierService.GetAllAsync(includeInactive);
|
||||
return Ok(suppliers.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<SupplierDto>> GetById(int id)
|
||||
{
|
||||
var supplier = await _supplierService.GetByIdAsync(id);
|
||||
if (supplier == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(supplier));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<SupplierDto>> Create(CreateSupplierDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
return BadRequest("Name is required");
|
||||
|
||||
var supplier = new Supplier
|
||||
{
|
||||
Name = dto.Name,
|
||||
ContactInfo = dto.ContactInfo,
|
||||
Notes = dto.Notes
|
||||
};
|
||||
|
||||
await _supplierService.CreateAsync(supplier);
|
||||
return CreatedAtAction(nameof(GetById), new { id = supplier.Id }, MapToDto(supplier));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<SupplierDto>> Update(int id, UpdateSupplierDto dto)
|
||||
{
|
||||
var supplier = await _supplierService.GetByIdAsync(id);
|
||||
if (supplier == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Name != null) supplier.Name = dto.Name;
|
||||
if (dto.ContactInfo != null) supplier.ContactInfo = dto.ContactInfo;
|
||||
if (dto.Notes != null) supplier.Notes = dto.Notes;
|
||||
|
||||
await _supplierService.UpdateAsync(supplier);
|
||||
return Ok(MapToDto(supplier));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var supplier = await _supplierService.GetByIdAsync(id);
|
||||
if (supplier == null)
|
||||
return NotFound();
|
||||
|
||||
await _supplierService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// --- Offerings ---
|
||||
|
||||
[HttpGet("{id}/offerings")]
|
||||
public async Task<ActionResult<List<OfferingDto>>> GetOfferings(int id)
|
||||
{
|
||||
var supplier = await _supplierService.GetByIdAsync(id);
|
||||
if (supplier == null)
|
||||
return NotFound();
|
||||
|
||||
var offerings = await _supplierService.GetOfferingsForSupplierAsync(id);
|
||||
return Ok(offerings.Select(MapOfferingToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/offerings")]
|
||||
public async Task<ActionResult<OfferingDto>> CreateOffering(int id, CreateOfferingDto dto)
|
||||
{
|
||||
var supplier = await _supplierService.GetByIdAsync(id);
|
||||
if (supplier == null)
|
||||
return NotFound();
|
||||
|
||||
var exists = await _supplierService.OfferingExistsAsync(id, dto.StockItemId);
|
||||
if (exists)
|
||||
return Conflict("An offering for this supplier and stock item already exists");
|
||||
|
||||
var offering = new SupplierOffering
|
||||
{
|
||||
SupplierId = id,
|
||||
StockItemId = dto.StockItemId,
|
||||
PartNumber = dto.PartNumber,
|
||||
SupplierDescription = dto.SupplierDescription,
|
||||
Price = dto.Price,
|
||||
Notes = dto.Notes
|
||||
};
|
||||
|
||||
await _supplierService.AddOfferingAsync(offering);
|
||||
|
||||
// Reload with includes
|
||||
var created = await _supplierService.GetOfferingByIdAsync(offering.Id);
|
||||
return CreatedAtAction(nameof(GetOfferings), new { id }, MapOfferingToDto(created!));
|
||||
}
|
||||
|
||||
[HttpPut("{supplierId}/offerings/{offeringId}")]
|
||||
public async Task<ActionResult<OfferingDto>> UpdateOffering(int supplierId, int offeringId, UpdateOfferingDto dto)
|
||||
{
|
||||
var offering = await _supplierService.GetOfferingByIdAsync(offeringId);
|
||||
if (offering == null || offering.SupplierId != supplierId)
|
||||
return NotFound();
|
||||
|
||||
if (dto.PartNumber != null) offering.PartNumber = dto.PartNumber;
|
||||
if (dto.SupplierDescription != null) offering.SupplierDescription = dto.SupplierDescription;
|
||||
if (dto.Price.HasValue) offering.Price = dto.Price;
|
||||
if (dto.Notes != null) offering.Notes = dto.Notes;
|
||||
|
||||
await _supplierService.UpdateOfferingAsync(offering);
|
||||
return Ok(MapOfferingToDto(offering));
|
||||
}
|
||||
|
||||
[HttpDelete("{supplierId}/offerings/{offeringId}")]
|
||||
public async Task<IActionResult> DeleteOffering(int supplierId, int offeringId)
|
||||
{
|
||||
var offering = await _supplierService.GetOfferingByIdAsync(offeringId);
|
||||
if (offering == null || offering.SupplierId != supplierId)
|
||||
return NotFound();
|
||||
|
||||
await _supplierService.DeleteOfferingAsync(offeringId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static SupplierDto MapToDto(Supplier s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = s.Name,
|
||||
ContactInfo = s.ContactInfo,
|
||||
Notes = s.Notes,
|
||||
IsActive = s.IsActive
|
||||
};
|
||||
|
||||
private static OfferingDto MapOfferingToDto(SupplierOffering o) => new()
|
||||
{
|
||||
Id = o.Id,
|
||||
SupplierId = o.SupplierId,
|
||||
SupplierName = o.Supplier?.Name,
|
||||
StockItemId = o.StockItemId,
|
||||
MaterialName = o.StockItem?.Material?.DisplayName,
|
||||
LengthInches = o.StockItem?.LengthInches,
|
||||
LengthFormatted = o.StockItem != null ? ArchUnits.FormatFromInches((double)o.StockItem.LengthInches) : null,
|
||||
PartNumber = o.PartNumber,
|
||||
SupplierDescription = o.SupplierDescription,
|
||||
Price = o.Price,
|
||||
Notes = o.Notes,
|
||||
IsActive = o.IsActive
|
||||
};
|
||||
}
|
||||
24
CutList.Web/DTOs/CuttingToolDtos.cs
Normal file
24
CutList.Web/DTOs/CuttingToolDtos.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class CuttingToolDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CreateCuttingToolDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal KerfInches { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateCuttingToolDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public decimal? KerfInches { get; set; }
|
||||
public bool? IsDefault { get; set; }
|
||||
}
|
||||
111
CutList.Web/DTOs/JobDtos.cs
Normal file
111
CutList.Web/DTOs/JobDtos.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class JobDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? CuttingToolName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public int StockCount { get; set; }
|
||||
}
|
||||
|
||||
public class JobDetailDto : JobDto
|
||||
{
|
||||
public List<JobPartDto> Parts { get; set; } = new();
|
||||
public List<JobStockDto> Stock { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreateJobDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateJobDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Customer { get; set; }
|
||||
public int? CuttingToolId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class JobPartDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class CreateJobPartDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class UpdateJobPartDto
|
||||
{
|
||||
public int? MaterialId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Length { get; set; }
|
||||
public int? Quantity { get; set; }
|
||||
}
|
||||
|
||||
public class JobStockDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int? StockItemId { get; set; }
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class CreateJobStockDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public int? StockItemId { get; set; }
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
public bool IsCustomLength { get; set; }
|
||||
public int Priority { get; set; } = 10;
|
||||
}
|
||||
|
||||
public class UpdateJobStockDto
|
||||
{
|
||||
public int? StockItemId { get; set; }
|
||||
public string? Length { get; set; }
|
||||
public int? Quantity { get; set; }
|
||||
public bool? IsCustomLength { get; set; }
|
||||
public int? Priority { get; set; }
|
||||
}
|
||||
|
||||
public class QuickCreateJobDto
|
||||
{
|
||||
public string? Customer { get; set; }
|
||||
}
|
||||
|
||||
public class PackJobRequestDto
|
||||
{
|
||||
public decimal? KerfOverride { get; set; }
|
||||
}
|
||||
53
CutList.Web/DTOs/MaterialDtos.cs
Normal file
53
CutList.Web/DTOs/MaterialDtos.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class MaterialDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? Grade { get; set; }
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public MaterialDimensionsDto? Dimensions { get; set; }
|
||||
}
|
||||
|
||||
public class CreateMaterialDto
|
||||
{
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string? Type { get; set; }
|
||||
public string? Grade { get; set; }
|
||||
public string? Size { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public Dictionary<string, decimal>? Dimensions { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateMaterialDto
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Grade { get; set; }
|
||||
public string? Size { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool? RegenerateSize { get; set; }
|
||||
public Dictionary<string, decimal>? Dimensions { get; set; }
|
||||
}
|
||||
|
||||
public class BulkCreateResult
|
||||
{
|
||||
public int Created { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MaterialDimensionsDto
|
||||
{
|
||||
public string DimensionType { get; set; } = string.Empty;
|
||||
public Dictionary<string, decimal> Values { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MaterialSearchDto
|
||||
{
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public decimal TargetValue { get; set; }
|
||||
public decimal Tolerance { get; set; } = 0.1m;
|
||||
}
|
||||
66
CutList.Web/DTOs/PackResultDtos.cs
Normal file
66
CutList.Web/DTOs/PackResultDtos.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class PackResponseDto
|
||||
{
|
||||
public List<MaterialPackResultDto> Materials { get; set; } = new();
|
||||
public PackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MaterialPackResultDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public List<PackedBinDto> InStockBins { get; set; } = new();
|
||||
public List<PackedBinDto> ToBePurchasedBins { get; set; } = new();
|
||||
public List<PackedItemDto> ItemsNotPlaced { get; set; } = new();
|
||||
public MaterialPackingSummaryDto Summary { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PackedBinDto
|
||||
{
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public double UsedInches { get; set; }
|
||||
public string UsedFormatted { get; set; } = string.Empty;
|
||||
public double WasteInches { get; set; }
|
||||
public string WasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public List<PackedItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PackedItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PackingSummaryDto
|
||||
{
|
||||
public int TotalInStockBins { get; set; }
|
||||
public int TotalToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public string TotalMaterialFormatted { get; set; } = string.Empty;
|
||||
public double TotalUsedInches { get; set; }
|
||||
public string TotalUsedFormatted { get; set; } = string.Empty;
|
||||
public double TotalWasteInches { get; set; }
|
||||
public string TotalWasteFormatted { get; set; } = string.Empty;
|
||||
public double Efficiency { get; set; }
|
||||
public int TotalItemsNotPlaced { get; set; }
|
||||
public List<MaterialPackingSummaryDto> MaterialSummaries { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MaterialPackingSummaryDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public int InStockBins { get; set; }
|
||||
public int ToBePurchasedBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterialInches { get; set; }
|
||||
public double TotalUsedInches { get; set; }
|
||||
public double TotalWasteInches { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
45
CutList.Web/DTOs/PackingDtos.cs
Normal file
45
CutList.Web/DTOs/PackingDtos.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class StandalonePackRequestDto
|
||||
{
|
||||
public List<PartInputDto> Parts { get; set; } = new();
|
||||
public List<StockBinInputDto> StockBins { get; set; } = new();
|
||||
public decimal Kerf { get; set; } = 0.125m;
|
||||
public string Strategy { get; set; } = "advanced";
|
||||
}
|
||||
|
||||
public class PartInputDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class StockBinInputDto
|
||||
{
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = -1;
|
||||
public int Priority { get; set; } = 25;
|
||||
}
|
||||
|
||||
public class ParseLengthRequestDto
|
||||
{
|
||||
public string Input { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ParseLengthResponseDto
|
||||
{
|
||||
public double Inches { get; set; }
|
||||
public string Formatted { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FormatLengthRequestDto
|
||||
{
|
||||
public double Inches { get; set; }
|
||||
}
|
||||
|
||||
public class FormatLengthResponseDto
|
||||
{
|
||||
public string Formatted { get; set; } = string.Empty;
|
||||
public double Inches { get; set; }
|
||||
}
|
||||
78
CutList.Web/DTOs/StockItemDtos.cs
Normal file
78
CutList.Web/DTOs/StockItemDtos.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class StockItemDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public int QuantityOnHand { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CreateStockItemDto
|
||||
{
|
||||
public int MaterialId { get; set; }
|
||||
public string Length { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public int QuantityOnHand { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateStockItemDto
|
||||
{
|
||||
public string? Length { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class StockTransactionDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockItemId { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public int? JobId { get; set; }
|
||||
public string? JobNumber { get; set; }
|
||||
public int? SupplierId { get; set; }
|
||||
public string? SupplierName { get; set; }
|
||||
public decimal? UnitPrice { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class AddStockDto
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public int? SupplierId { get; set; }
|
||||
public decimal? UnitPrice { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UseStockDto
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public int? JobId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class AdjustStockDto
|
||||
{
|
||||
public int NewQuantity { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class ScrapStockDto
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class StockPricingDto
|
||||
{
|
||||
public decimal? AverageCost { get; set; }
|
||||
public decimal? LastPurchasePrice { get; set; }
|
||||
}
|
||||
57
CutList.Web/DTOs/SupplierDtos.cs
Normal file
57
CutList.Web/DTOs/SupplierDtos.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace CutList.Web.DTOs;
|
||||
|
||||
public class SupplierDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? ContactInfo { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CreateSupplierDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? ContactInfo { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateSupplierDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? ContactInfo { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class OfferingDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SupplierId { get; set; }
|
||||
public string? SupplierName { get; set; }
|
||||
public int StockItemId { get; set; }
|
||||
public string? MaterialName { get; set; }
|
||||
public decimal? LengthInches { get; set; }
|
||||
public string? LengthFormatted { get; set; }
|
||||
public string? PartNumber { get; set; }
|
||||
public string? SupplierDescription { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class CreateOfferingDto
|
||||
{
|
||||
public int StockItemId { get; set; }
|
||||
public string? PartNumber { get; set; }
|
||||
public string? SupplierDescription { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateOfferingDto
|
||||
{
|
||||
public string? PartNumber { get; set; }
|
||||
public string? SupplierDescription { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user