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