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:
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user