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>> GetAll( [FromQuery] bool includeInactive = false, [FromQuery] int? materialId = null) { List 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> GetById(int id) { var item = await _stockItemService.GetByIdAsync(id); if (item == null) return NotFound(); return Ok(MapToDto(item)); } [HttpPost] public async Task> 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> 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 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>> GetByMaterial(int materialId) { var items = await _stockItemService.GetByMaterialAsync(materialId); return Ok(items.Select(MapToDto).ToList()); } [HttpGet("{id}/offerings")] public async Task>> 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> 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>> 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> 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> 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> 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> 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> 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 }; }