using Extrudex.API.DTOs.Materials; using Extrudex.API.Validators; using Extrudex.Domain.Entities; using Extrudex.Infrastructure.Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Extrudex.API.Controllers; /// /// Controller for querying material metadata — bases, finishes, and modifiers. /// Provides lookup/reference data for the spool inventory system. /// [ApiController] [Route("api/materials")] public class MaterialLookupsController : ControllerBase { private readonly ExtrudexDbContext _dbContext; private readonly ILogger _logger; public MaterialLookupsController(ExtrudexDbContext dbContext, ILogger logger) { _dbContext = dbContext; _logger = logger; } // ── MaterialBase ────────────────────────────────────────── /// /// Gets all material bases (PLA, PETG, ABS, etc.). /// /// A list of all material bases with their densities. [HttpGet("bases")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetMaterialBases() { _logger.LogDebug("Getting all material bases"); var bases = await _dbContext.MaterialBases .OrderBy(mb => mb.Name) .Select(mb => new MaterialBaseResponse { Id = mb.Id, Name = mb.Name, DensityGperCm3 = mb.DensityGperCm3, CreatedAt = mb.CreatedAt, UpdatedAt = mb.UpdatedAt }) .ToListAsync(); return Ok(bases); } /// /// Gets a specific material base by ID. /// /// The unique identifier of the material base. /// The material base details. [HttpGet("bases/{id:guid}")] [ProducesResponseType(typeof(MaterialBaseResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetMaterialBase(Guid id) { _logger.LogDebug("Getting material base {Id}", id); var mb = await _dbContext.MaterialBases.FindAsync(id); if (mb is null) { _logger.LogWarning("Material base {Id} not found", id); return NotFound(new { error = $"Material base with ID '{id}' not found." }); } return Ok(MapToMaterialBaseResponse(mb)); } /// /// Creates a new material base. /// /// The material base creation request. /// The created material base. [HttpPost("bases")] [ProducesResponseType(typeof(MaterialBaseResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateMaterialBase( [FromBody] CreateMaterialBaseRequest request) { _logger.LogInformation("Creating material base: {Name}", request.Name); var entity = new MaterialBase { Name = request.Name, DensityGperCm3 = request.DensityGperCm3 }; _dbContext.MaterialBases.Add(entity); await _dbContext.SaveChangesAsync(); var response = MapToMaterialBaseResponse(entity); return CreatedAtAction(nameof(GetMaterialBase), new { id = entity.Id }, response); } /// /// Updates an existing material base. /// /// The unique identifier of the material base to update. /// The material base update request. /// The updated material base. [HttpPut("bases/{id:guid}")] [ProducesResponseType(typeof(MaterialBaseResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> UpdateMaterialBase( Guid id, [FromBody] UpdateMaterialBaseRequest request) { _logger.LogInformation("Updating material base {Id}", id); var entity = await _dbContext.MaterialBases.FindAsync(id); if (entity is null) { _logger.LogWarning("Material base {Id} not found for update", id); return NotFound(new { error = $"Material base with ID '{id}' not found." }); } entity.Name = request.Name; entity.DensityGperCm3 = request.DensityGperCm3; await _dbContext.SaveChangesAsync(); return Ok(MapToMaterialBaseResponse(entity)); } /// /// Deletes a material base. /// /// The unique identifier of the material base to delete. [HttpDelete("bases/{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteMaterialBase(Guid id) { _logger.LogInformation("Deleting material base {Id}", id); var entity = await _dbContext.MaterialBases.FindAsync(id); if (entity is null) { _logger.LogWarning("Material base {Id} not found for deletion", id); return NotFound(new { error = $"Material base with ID '{id}' not found." }); } _dbContext.MaterialBases.Remove(entity); await _dbContext.SaveChangesAsync(); return NoContent(); } // ── MaterialFinish ──────────────────────────────────────── /// /// Gets all material finishes, optionally filtered by material base. /// /// Optional filter: return finishes for this material base only. /// A list of material finishes. [HttpGet("finishes")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetMaterialFinishes( [FromQuery] Guid? materialBaseId) { _logger.LogDebug("Getting material finishes, filter: {MaterialBaseId}", materialBaseId); var query = _dbContext.MaterialFinishes .Include(mf => mf.MaterialBase) .AsQueryable(); if (materialBaseId.HasValue) { query = query.Where(mf => mf.MaterialBaseId == materialBaseId.Value); } var finishes = await query .OrderBy(mf => mf.MaterialBase.Name) .ThenBy(mf => mf.Name) .Select(mf => new MaterialFinishResponse { Id = mf.Id, Name = mf.Name, MaterialBaseId = mf.MaterialBaseId, MaterialBaseName = mf.MaterialBase.Name, CreatedAt = mf.CreatedAt, UpdatedAt = mf.UpdatedAt }) .ToListAsync(); return Ok(finishes); } /// /// Gets a specific material finish by ID. /// /// The unique identifier of the finish. /// The material finish details. [HttpGet("finishes/{id:guid}")] [ProducesResponseType(typeof(MaterialFinishResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetMaterialFinish(Guid id) { _logger.LogDebug("Getting material finish {Id}", id); var mf = await _dbContext.MaterialFinishes .Include(mf => mf.MaterialBase) .FirstOrDefaultAsync(mf => mf.Id == id); if (mf is null) { _logger.LogWarning("Material finish {Id} not found", id); return NotFound(new { error = $"Material finish with ID '{id}' not found." }); } return Ok(MapToMaterialFinishResponse(mf)); } /// /// Creates a new material finish. /// /// The material finish creation request. /// The created material finish. [HttpPost("finishes")] [ProducesResponseType(typeof(MaterialFinishResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> CreateMaterialFinish( [FromBody] CreateMaterialFinishRequest request) { _logger.LogInformation("Creating material finish: {Name} for base {MaterialBaseId}", request.Name, request.MaterialBaseId); var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId); if (materialBase is null) { return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." }); } var entity = new MaterialFinish { Name = request.Name, MaterialBaseId = request.MaterialBaseId }; _dbContext.MaterialFinishes.Add(entity); await _dbContext.SaveChangesAsync(); var response = MapToMaterialFinishResponse(entity, materialBase.Name); return CreatedAtAction(nameof(GetMaterialFinish), new { id = entity.Id }, response); } /// /// Updates an existing material finish. /// /// The unique identifier of the finish to update. /// The material finish update request. /// The updated material finish. [HttpPut("finishes/{id:guid}")] [ProducesResponseType(typeof(MaterialFinishResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> UpdateMaterialFinish( Guid id, [FromBody] UpdateMaterialFinishRequest request) { _logger.LogInformation("Updating material finish {Id}", id); var entity = await _dbContext.MaterialFinishes.FindAsync(id); if (entity is null) { _logger.LogWarning("Material finish {Id} not found for update", id); return NotFound(new { error = $"Material finish with ID '{id}' not found." }); } var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId); if (materialBase is null) { return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." }); } entity.Name = request.Name; entity.MaterialBaseId = request.MaterialBaseId; await _dbContext.SaveChangesAsync(); return Ok(MapToMaterialFinishResponse(entity, materialBase.Name)); } /// /// Deletes a material finish. /// /// The unique identifier of the finish to delete. [HttpDelete("finishes/{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteMaterialFinish(Guid id) { _logger.LogInformation("Deleting material finish {Id}", id); var entity = await _dbContext.MaterialFinishes.FindAsync(id); if (entity is null) { _logger.LogWarning("Material finish {Id} not found for deletion", id); return NotFound(new { error = $"Material finish with ID '{id}' not found." }); } _dbContext.MaterialFinishes.Remove(entity); await _dbContext.SaveChangesAsync(); return NoContent(); } // ── MaterialModifier ────────────────────────────────────── /// /// Gets all material modifiers, optionally filtered by material base. /// /// Optional filter: return modifiers for this material base only. /// A list of material modifiers. [HttpGet("modifiers")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetMaterialModifiers( [FromQuery] Guid? materialBaseId) { _logger.LogDebug("Getting material modifiers, filter: {MaterialBaseId}", materialBaseId); var query = _dbContext.MaterialModifiers .Include(mm => mm.MaterialBase) .AsQueryable(); if (materialBaseId.HasValue) { query = query.Where(mm => mm.MaterialBaseId == materialBaseId.Value); } var modifiers = await query .OrderBy(mm => mm.MaterialBase.Name) .ThenBy(mm => mm.Name) .Select(mm => new MaterialModifierResponse { Id = mm.Id, Name = mm.Name, MaterialBaseId = mm.MaterialBaseId, MaterialBaseName = mm.MaterialBase.Name, CreatedAt = mm.CreatedAt, UpdatedAt = mm.UpdatedAt }) .ToListAsync(); return Ok(modifiers); } /// /// Gets a specific material modifier by ID. /// /// The unique identifier of the modifier. /// The material modifier details. [HttpGet("modifiers/{id:guid}")] [ProducesResponseType(typeof(MaterialModifierResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetMaterialModifier(Guid id) { _logger.LogDebug("Getting material modifier {Id}", id); var mm = await _dbContext.MaterialModifiers .Include(mm => mm.MaterialBase) .FirstOrDefaultAsync(mm => mm.Id == id); if (mm is null) { _logger.LogWarning("Material modifier {Id} not found", id); return NotFound(new { error = $"Material modifier with ID '{id}' not found." }); } return Ok(MapToMaterialModifierResponse(mm)); } /// /// Creates a new material modifier. /// /// The material modifier creation request. /// The created material modifier. [HttpPost("modifiers")] [ProducesResponseType(typeof(MaterialModifierResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> CreateMaterialModifier( [FromBody] CreateMaterialModifierRequest request) { _logger.LogInformation("Creating material modifier: {Name} for base {MaterialBaseId}", request.Name, request.MaterialBaseId); var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId); if (materialBase is null) { return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." }); } var entity = new MaterialModifier { Name = request.Name, MaterialBaseId = request.MaterialBaseId }; _dbContext.MaterialModifiers.Add(entity); await _dbContext.SaveChangesAsync(); var response = MapToMaterialModifierResponse(entity, materialBase.Name); return CreatedAtAction(nameof(GetMaterialModifier), new { id = entity.Id }, response); } /// /// Updates an existing material modifier. /// /// The unique identifier of the modifier to update. /// The material modifier update request. /// The updated material modifier. [HttpPut("modifiers/{id:guid}")] [ProducesResponseType(typeof(MaterialModifierResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> UpdateMaterialModifier( Guid id, [FromBody] UpdateMaterialModifierRequest request) { _logger.LogInformation("Updating material modifier {Id}", id); var entity = await _dbContext.MaterialModifiers.FindAsync(id); if (entity is null) { _logger.LogWarning("Material modifier {Id} not found for update", id); return NotFound(new { error = $"Material modifier with ID '{id}' not found." }); } var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId); if (materialBase is null) { return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." }); } entity.Name = request.Name; entity.MaterialBaseId = request.MaterialBaseId; await _dbContext.SaveChangesAsync(); return Ok(MapToMaterialModifierResponse(entity, materialBase.Name)); } /// /// Deletes a material modifier. /// /// The unique identifier of the modifier to delete. [HttpDelete("modifiers/{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteMaterialModifier(Guid id) { _logger.LogInformation("Deleting material modifier {Id}", id); var entity = await _dbContext.MaterialModifiers.FindAsync(id); if (entity is null) { _logger.LogWarning("Material modifier {Id} not found for deletion", id); return NotFound(new { error = $"Material modifier with ID '{id}' not found." }); } _dbContext.MaterialModifiers.Remove(entity); await _dbContext.SaveChangesAsync(); return NoContent(); } // ── Density convenience endpoint ───────────────────────── /// /// Gets all material density data (shorthand for bases with density info). /// /// A list of material bases with their density values. [HttpGet("densities")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetMaterialDensities() { _logger.LogDebug("Getting material densities"); var bases = await _dbContext.MaterialBases .OrderBy(mb => mb.Name) .Select(mb => new MaterialBaseResponse { Id = mb.Id, Name = mb.Name, DensityGperCm3 = mb.DensityGperCm3, CreatedAt = mb.CreatedAt, UpdatedAt = mb.UpdatedAt }) .ToListAsync(); return Ok(bases); } // ── Mapping helpers ─────────────────────────────────────── private static MaterialBaseResponse MapToMaterialBaseResponse(MaterialBase mb) => new() { Id = mb.Id, Name = mb.Name, DensityGperCm3 = mb.DensityGperCm3, CreatedAt = mb.CreatedAt, UpdatedAt = mb.UpdatedAt }; private static MaterialFinishResponse MapToMaterialFinishResponse( MaterialFinish mf, string? materialBaseName = null) => new() { Id = mf.Id, Name = mf.Name, MaterialBaseId = mf.MaterialBaseId, MaterialBaseName = materialBaseName ?? mf.MaterialBase?.Name ?? string.Empty, CreatedAt = mf.CreatedAt, UpdatedAt = mf.UpdatedAt }; private static MaterialModifierResponse MapToMaterialModifierResponse( MaterialModifier mm, string? materialBaseName = null) => new() { Id = mm.Id, Name = mm.Name, MaterialBaseId = mm.MaterialBaseId, MaterialBaseName = materialBaseName ?? mm.MaterialBase?.Name ?? string.Empty, CreatedAt = mm.CreatedAt, UpdatedAt = mm.UpdatedAt }; }