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>> GetAll() { var jobs = await _jobService.GetAllAsync(); return Ok(jobs.Select(MapToDto).ToList()); } [HttpGet("{id}")] public async Task> GetById(int id) { var job = await _jobService.GetByIdAsync(id); if (job == null) return NotFound(); return Ok(MapToDetailDto(job)); } [HttpPost] public async Task> 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> 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> 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 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> 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>> 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> 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> 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 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>> 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> 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> 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 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>> 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> 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() }; }