Initial commit
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Layout;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapEditor.Shared.Models;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing layouts, versions, and levels
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/layouts")]
|
||||
[Authorize]
|
||||
public class LayoutManagerController(
|
||||
ILayoutService layoutService,
|
||||
IImageStorageService imageStorageService,
|
||||
ILogger<LayoutManagerController> logger) : ControllerBase
|
||||
{
|
||||
private readonly ILayoutService _layoutService = layoutService;
|
||||
private readonly IImageStorageService _imageStorageService = imageStorageService;
|
||||
private readonly ILogger<LayoutManagerController> _logger = logger;
|
||||
|
||||
// ==========================================
|
||||
// LAYOUT OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Create a new layout
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(LayoutDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<LayoutDto>> CreateLayout([FromBody] CreateLayoutRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var layout = await _layoutService.CreateLayoutAsync(request);
|
||||
var dto = MapLayoutToDto(layout);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetLayout),
|
||||
new { layoutId = layout.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to create layout: {LayoutId}", request.LayoutId);
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search layouts by text
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<LayoutDto>), 200)]
|
||||
public async Task<ActionResult<List<LayoutDto>>> SearchLayouts([FromQuery] string? search)
|
||||
{
|
||||
var layouts = await _layoutService.SearchLayoutsAsync(search);
|
||||
var dtos = layouts.Select(MapLayoutToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get layout by database ID
|
||||
/// </summary>
|
||||
[HttpGet("{layoutId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> GetLayout(Guid layoutId)
|
||||
{
|
||||
var layout = await _layoutService.GetLayoutByIdAsync(layoutId);
|
||||
|
||||
if (layout == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Layout with ID '{layoutId}' not found", "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get layout by layout ID string
|
||||
/// </summary>
|
||||
[HttpGet("by-id/{layoutId}")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> GetLayoutByLayoutId(string layoutId)
|
||||
{
|
||||
var layout = await _layoutService.GetLayoutByLayoutIdAsync(layoutId);
|
||||
|
||||
if (layout == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Layout with ID '{layoutId}' not found", "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get layout by name
|
||||
/// </summary>
|
||||
[HttpGet("by-name/{layoutName}")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> GetLayoutByName(string layoutName)
|
||||
{
|
||||
var layout = await _layoutService.GetLayoutByNameAsync(layoutName);
|
||||
|
||||
if (layout == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Layout with name '{layoutName}' not found", "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update layout
|
||||
/// </summary>
|
||||
[HttpPut("{layoutId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> UpdateLayout(
|
||||
Guid layoutId,
|
||||
[FromBody] UpdateLayoutRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var layout = await _layoutService.UpdateLayoutAsync(layoutId, request);
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update layout: {LayoutId}", layoutId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "LAYOUT_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete layout (must be deactivated first)
|
||||
/// Hard delete with cascade
|
||||
/// </summary>
|
||||
[HttpDelete("{layoutId:guid}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteLayout(Guid layoutId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _layoutService.DeleteLayoutAsync(layoutId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Layout with ID '{layoutId}' not found", "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to delete layout: {LayoutId}", layoutId);
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate layout
|
||||
/// </summary>
|
||||
[HttpPost("{layoutId:guid}/activate")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> ActivateLayout(Guid layoutId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var layout = await _layoutService.ActivateLayoutAsync(layoutId);
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to activate layout: {LayoutId}", layoutId);
|
||||
return NotFound(CreateErrorResponse(ex.Message, "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate layout
|
||||
/// </summary>
|
||||
[HttpPost("{layoutId:guid}/deactivate")]
|
||||
[ProducesResponseType(typeof(LayoutDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutDto>> DeactivateLayout(Guid layoutId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var layout = await _layoutService.DeactivateLayoutAsync(layoutId);
|
||||
|
||||
return Ok(MapLayoutToDto(layout));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to deactivate layout: {LayoutId}", layoutId);
|
||||
return NotFound(CreateErrorResponse(ex.Message, "LAYOUT_NOT_FOUND"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// VERSION OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Create new version for a layout
|
||||
/// </summary>
|
||||
[HttpPost("{layoutId:guid}/versions")]
|
||||
[ProducesResponseType(typeof(LayoutVersionDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutVersionDto>> CreateVersion(
|
||||
Guid layoutId,
|
||||
[FromBody] CreateLayoutVersionRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = await _layoutService.CreateVersionAsync(layoutId, request);
|
||||
var dto = MapVersionToDto(version);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetVersion),
|
||||
new { versionId = version.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to create version for layout: {LayoutId}", layoutId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "LAYOUT_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all versions for a layout
|
||||
/// </summary>
|
||||
[HttpGet("{layoutId:guid}/versions")]
|
||||
[ProducesResponseType(typeof(List<LayoutVersionDto>), 200)]
|
||||
public async Task<ActionResult<List<LayoutVersionDto>>> GetVersions(Guid layoutId)
|
||||
{
|
||||
var versions = await _layoutService.GetVersionsAsync(layoutId);
|
||||
var dtos = versions.Select(MapVersionToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get version by ID
|
||||
/// </summary>
|
||||
[HttpGet("versions/{versionId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutVersionDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutVersionDto>> GetVersion(Guid versionId)
|
||||
{
|
||||
var version = await _layoutService.GetVersionAsync(versionId);
|
||||
|
||||
if (version == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Version with ID '{versionId}' not found", "VERSION_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapVersionToDto(version));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update version
|
||||
/// </summary>
|
||||
[HttpPut("versions/{versionId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutVersionDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutVersionDto>> UpdateVersion(
|
||||
Guid versionId,
|
||||
[FromBody] UpdateLayoutRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = await _layoutService.UpdateVersionAsync(versionId, request);
|
||||
|
||||
return Ok(MapVersionToDto(version));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update version: {VersionId}", versionId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "VERSION_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete version (layout must be deactivated first)
|
||||
/// </summary>
|
||||
[HttpDelete("versions/{versionId:guid}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteVersion(Guid versionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _layoutService.DeleteVersionAsync(versionId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Version with ID '{versionId}' not found", "VERSION_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to delete version: {VersionId}", versionId);
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// LEVEL OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Create new level for a version
|
||||
/// </summary>
|
||||
[HttpPost("versions/{versionId:guid}/levels")]
|
||||
[ProducesResponseType(typeof(LayoutLevelDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutLevelDto>> CreateLevel(
|
||||
Guid versionId,
|
||||
[FromBody] CreateLayoutLevelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var level = await _layoutService.CreateLevelAsync(versionId, request);
|
||||
var dto = MapLevelToDto(level);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetLevel),
|
||||
new { levelId = level.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to create level for version: {VersionId}", versionId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "VERSION_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new layout level with background image in a single request
|
||||
/// </summary>
|
||||
/// <param name="versionId">Version ID</param>
|
||||
/// <param name="layoutLevelId">Layout level identifier string</param>
|
||||
/// <param name="levelOrder">Level order</param>
|
||||
/// <param name="resolution">Resolution in meters per pixel</param>
|
||||
/// <param name="originX">Origin X coordinate in meters</param>
|
||||
/// <param name="originY">Origin Y coordinate in meters</param>
|
||||
/// <param name="file">Background image file (PNG format, required)</param>
|
||||
/// <returns>Created level with image metadata</returns>
|
||||
[HttpPost("versions/{versionId:guid}/levels/with-image")]
|
||||
[ProducesResponseType(typeof(LayoutLevelDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutLevelDto>> CreateLevelWithImage(
|
||||
Guid versionId,
|
||||
[FromForm] string layoutLevelId,
|
||||
[FromForm] int levelOrder,
|
||||
[FromForm] double resolution,
|
||||
[FromForm] double originX,
|
||||
[FromForm] double originY,
|
||||
IFormFile file)
|
||||
{
|
||||
// Validate file
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(CreateErrorResponse("Image file is required", "VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.ContentType.Equals("image/png", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest(CreateErrorResponse("Only PNG images are supported", "VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
// Validate file size (max 10MB)
|
||||
const long maxFileSize = 10 * 1024 * 1024;
|
||||
if (file.Length > maxFileSize)
|
||||
{
|
||||
return BadRequest(CreateErrorResponse($"File size exceeds maximum of {maxFileSize / 1024 / 1024}MB", "VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Extract image dimensions
|
||||
int imageWidth, imageHeight;
|
||||
using (var stream = file.OpenReadStream())
|
||||
{
|
||||
(imageWidth, imageHeight) = await _imageStorageService.GetImageDimensionsAsync(stream);
|
||||
}
|
||||
|
||||
// Step 2: Create level with complete coordinate system info
|
||||
var request = new CreateLayoutLevelRequest
|
||||
{
|
||||
LayoutLevelId = layoutLevelId,
|
||||
LevelOrder = levelOrder,
|
||||
CoordinateSystem = new CoordinateSystemInfo
|
||||
{
|
||||
Resolution = resolution,
|
||||
OriginX = originX,
|
||||
OriginY = originY,
|
||||
ImageWidth = imageWidth,
|
||||
ImageHeight = imageHeight,
|
||||
// Calculate bounds based on image size
|
||||
BoundsMinX = originX,
|
||||
BoundsMaxX = imageWidth * resolution + originX,
|
||||
BoundsMinY = originY,
|
||||
BoundsMaxY = imageHeight * resolution + originY
|
||||
}
|
||||
};
|
||||
|
||||
var level = await _layoutService.CreateLevelAsync(versionId, request);
|
||||
|
||||
// Step 3: Upload image
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await _imageStorageService.SaveImageAsync(level.Id, stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to save image after creating level {LevelId}, attempting rollback", level.Id);
|
||||
|
||||
// Attempt to delete the created level to maintain consistency
|
||||
try
|
||||
{
|
||||
await _layoutService.DeleteLevelAsync(level.Id);
|
||||
}
|
||||
catch (Exception rollbackEx)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(rollbackEx, "Failed to rollback level creation for {LevelId}", level.Id);
|
||||
}
|
||||
|
||||
return StatusCode(500, CreateErrorResponse("Failed to save image. Level creation was rolled back.", "INTERNAL_ERROR"));
|
||||
}
|
||||
|
||||
var dto = MapLevelToDto(level);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetLevel),
|
||||
new { levelId = level.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to create level with image for version: {VersionId}", versionId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "VERSION_NOT_FOUND"));
|
||||
|
||||
if (ex.Message.Contains("Invalid image") || ex.Message.Contains("corrupted"))
|
||||
return BadRequest(CreateErrorResponse("Invalid or corrupted image file", "VALIDATION_ERROR"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Unexpected error creating level with image for version {VersionId}", versionId);
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while creating level with image", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all levels for a version
|
||||
/// </summary>
|
||||
[HttpGet("versions/{versionId:guid}/levels")]
|
||||
[ProducesResponseType(typeof(List<LayoutLevelDto>), 200)]
|
||||
public async Task<ActionResult<List<LayoutLevelDto>>> GetLevels(Guid versionId)
|
||||
{
|
||||
var levels = await _layoutService.GetLevelsAsync(versionId);
|
||||
var dtos = levels.Select(MapLevelToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get level by ID
|
||||
/// </summary>
|
||||
[HttpGet("levels/{levelId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutLevelDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutLevelDto>> GetLevel(Guid levelId)
|
||||
{
|
||||
var level = await _layoutService.GetLevelAsync(levelId);
|
||||
|
||||
if (level == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Level with ID '{levelId}' not found", "LEVEL_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapLevelToDto(level));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update level
|
||||
/// </summary>
|
||||
[HttpPut("levels/{levelId:guid}")]
|
||||
[ProducesResponseType(typeof(LayoutLevelDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<LayoutLevelDto>> UpdateLevel(
|
||||
Guid levelId,
|
||||
[FromBody] UpdateLayoutLevelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var level = await _layoutService.UpdateLevelAsync(levelId, request);
|
||||
|
||||
return Ok(MapLevelToDto(level));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update level: {LevelId}", levelId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "LEVEL_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete level (layout must be deactivated first)
|
||||
/// </summary>
|
||||
[HttpDelete("levels/{levelId:guid}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteLevel(Guid levelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _layoutService.DeleteLevelAsync(levelId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Level with ID '{levelId}' not found", "LEVEL_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to delete level: {LevelId}", levelId);
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HELPER METHODS
|
||||
// ==========================================
|
||||
|
||||
private static LayoutDto MapLayoutToDto(Data.Layout layout)
|
||||
{
|
||||
return new LayoutDto
|
||||
{
|
||||
Id = layout.Id,
|
||||
LayoutId = layout.LayoutId,
|
||||
LayoutName = layout.LayoutName,
|
||||
Description = layout.Description,
|
||||
IsActive = layout.IsActive,
|
||||
CreatedDate = layout.CreatedDate,
|
||||
ModifiedDate = layout.ModifiedDate,
|
||||
CreatedBy = layout.CreatedBy,
|
||||
ModifiedBy = layout.ModifiedBy,
|
||||
Versions = layout.Versions?.Select(MapVersionToDto).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static LayoutVersionDto MapVersionToDto(Data.LayoutVersion version)
|
||||
{
|
||||
return new LayoutVersionDto
|
||||
{
|
||||
Id = version.Id,
|
||||
LayoutId = version.LayoutId,
|
||||
Version = version.Version,
|
||||
LayoutDescription = version.LayoutDescription,
|
||||
CreatedBy = version.CreatedBy,
|
||||
CreatedDate = version.CreatedDate,
|
||||
IsActive = version.IsActive,
|
||||
Levels = version.Levels?.Select(MapLevelToDto).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static LayoutLevelDto MapLevelToDto(Data.LayoutLevel level)
|
||||
{
|
||||
return new LayoutLevelDto
|
||||
{
|
||||
Id = level.Id,
|
||||
VersionId = level.VersionId,
|
||||
LayoutLevelId = level.LayoutLevelId,
|
||||
LevelOrder = level.LevelOrder,
|
||||
EditorSettings = level.EditorSettings != null ? MapEditorSettingsToDto(level.EditorSettings) : null
|
||||
};
|
||||
}
|
||||
|
||||
private static LayoutLevelEditorSettingsDto MapEditorSettingsToDto(Data.LayoutLevelEditorSettings settings)
|
||||
{
|
||||
return new LayoutLevelEditorSettingsDto
|
||||
{
|
||||
Id = settings.Id,
|
||||
LevelId = settings.LevelId,
|
||||
EdgeMinLengthCreate = settings.EdgeMinLengthCreate,
|
||||
EdgeNameAutoGenerate = settings.EdgeNameAutoGenerate,
|
||||
NodeNameAutoGenerate = settings.NodeNameAutoGenerate,
|
||||
NodeProximityRadius = settings.NodeProximityRadius,
|
||||
OriginX = settings.OriginX,
|
||||
OriginY = settings.OriginY,
|
||||
Resolution = settings.Resolution,
|
||||
BoundsMinX = settings.BoundsMinX,
|
||||
BoundsMaxX = settings.BoundsMaxX,
|
||||
BoundsMinY = settings.BoundsMinY,
|
||||
BoundsMaxY = settings.BoundsMaxY,
|
||||
ImageWidth = settings.ImageWidth,
|
||||
ImageHeight = settings.ImageHeight,
|
||||
CreatedDate = settings.CreatedDate,
|
||||
ModifiedDate = settings.ModifiedDate
|
||||
};
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user