Initial commit
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Edge;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapManager.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing edges with complex node detection and cascade delete logic
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/edges")]
|
||||
[Authorize]
|
||||
public class EdgesController(
|
||||
IEdgeService edgeService,
|
||||
ILogger<EdgesController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IEdgeService _edgeService = edgeService;
|
||||
private readonly ILogger<EdgesController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get all edges for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>List of edges with nodes and vehicle properties</returns>
|
||||
[HttpGet("level/{layoutLevelId}")]
|
||||
[ProducesResponseType(typeof(List<EdgeDto>), 200)]
|
||||
public async Task<ActionResult<List<EdgeDto>>> GetEdgesByLevel(Guid layoutLevelId)
|
||||
{
|
||||
var edges = await _edgeService.GetEdgesByLevelAsync(layoutLevelId, includeNodes: true, includeVehicleProperties: true);
|
||||
var dtos = edges.Select(MapToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get edge by ID
|
||||
/// </summary>
|
||||
/// <param name="edgeId">Edge database ID</param>
|
||||
/// <returns>Edge details with nodes and vehicle properties</returns>
|
||||
[HttpGet("{edgeId}")]
|
||||
[ProducesResponseType(typeof(EdgeDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<EdgeDto>> GetEdge(Guid edgeId)
|
||||
{
|
||||
var edge = await _edgeService.GetByIdAsync(edgeId, includeNodes: true, includeVehicleProperties: true);
|
||||
|
||||
if (edge == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapToDto(edge));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create edge with automatic node detection/creation
|
||||
/// If start/end point is within NodeProximityRadius (default 0.35m) of existing node, connect to that node
|
||||
/// Otherwise, create new node at exact coordinates
|
||||
/// </summary>
|
||||
/// <param name="request">Edge creation request with coordinates in METERS</param>
|
||||
/// <returns>Created edge with connected nodes</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(EdgeDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<EdgeDto>> CreateEdge([FromBody] CreateEdgeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var edge = await _edgeService.CreateAsync(request);
|
||||
var dto = MapToDto(edge);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetEdge),
|
||||
new { edgeId = edge.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create edge");
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update edge properties
|
||||
/// </summary>
|
||||
/// <param name="edgeId">Edge database ID</param>
|
||||
/// <param name="request">Update request</param>
|
||||
/// <returns>Updated edge</returns>
|
||||
[HttpPut("{edgeId}")]
|
||||
[ProducesResponseType(typeof(EdgeDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<EdgeDto>> UpdateEdge(
|
||||
Guid edgeId,
|
||||
[FromBody] UpdateEdgeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var edge = await _edgeService.UpdateAsync(edgeId, request);
|
||||
|
||||
return Ok(MapToDto(edge));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if(_logger.IsEnabled(LogLevel.Warning))_logger.LogWarning(ex, "Failed to update edge: {edgeId}", edgeId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "EDGE_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete edge
|
||||
/// Cascade deletes orphan nodes (nodes not connected to any other edge)
|
||||
/// Also deletes StationInteractionNodes referencing orphan nodes
|
||||
/// </summary>
|
||||
/// <param name="edgeId">Edge database ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("{edgeId}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteEdge(Guid edgeId)
|
||||
{
|
||||
var deleted = await _edgeService.DeleteAsync(edgeId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete multiple edges in a transaction
|
||||
/// All edges are deleted or none (transaction)
|
||||
/// Cascade deletes orphan nodes and StationInteractionNodes
|
||||
/// </summary>
|
||||
/// <param name="request">Batch delete request with edge IDs</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("batch")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<IActionResult> DeleteEdgesBatch([FromBody] DeleteEdgesRequest request)
|
||||
{
|
||||
if (request.EdgeIds == null || request.EdgeIds.Count == 0)
|
||||
{
|
||||
return BadRequest(CreateErrorResponse("No edge IDs provided", "VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _edgeService.DeleteBatchAsync(request.EdgeIds);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to batch delete edges");
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to batch delete edges");
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while deleting edges", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to map entity to DTO
|
||||
private static EdgeDto MapToDto(Data.Edge edge)
|
||||
{
|
||||
return new EdgeDto
|
||||
{
|
||||
Id = edge.Id,
|
||||
LevelId = edge.LevelId,
|
||||
EdgeId = edge.EdgeId,
|
||||
EdgeName = edge.EdgeName,
|
||||
EdgeDescription = edge.EdgeDescription,
|
||||
StartNodeId = edge.StartNodeId,
|
||||
EndNodeId = edge.EndNodeId,
|
||||
StartNode = edge.StartNode != null ? new NodeDto
|
||||
{
|
||||
Id = edge.StartNode.Id,
|
||||
NodeId = edge.StartNode.NodeId,
|
||||
NodeName = edge.StartNode.NodeName,
|
||||
X = edge.StartNode.X,
|
||||
Y = edge.StartNode.Y
|
||||
} : null,
|
||||
EndNode = edge.EndNode != null ? new NodeDto
|
||||
{
|
||||
Id = edge.EndNode.Id,
|
||||
NodeId = edge.EndNode.NodeId,
|
||||
NodeName = edge.EndNode.NodeName,
|
||||
X = edge.EndNode.X,
|
||||
Y = edge.EndNode.Y
|
||||
} : null,
|
||||
VehicleProperties = edge.VehicleProperties?.Select(vp => new EdgeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
EdgeId = vp.EdgeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
VehicleTypeIdString = vp.VehicleType?.VehicleTypeId,
|
||||
VehicleOrientation = vp.VehicleOrientation,
|
||||
OrientationType = vp.OrientationType,
|
||||
RotationAllowed = vp.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = vp.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = vp.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = vp.MaxSpeed,
|
||||
MaxRotationSpeed = vp.MaxRotationSpeed,
|
||||
MinHeight = vp.MinHeight,
|
||||
MaxHeight = vp.MaxHeight,
|
||||
LoadRestriction = (vp.LoadRestriction_Unloaded.HasValue || vp.LoadRestriction_Loaded.HasValue || !string.IsNullOrWhiteSpace(vp.LoadRestriction_LoadSetNames))
|
||||
? new LoadRestrictionDto
|
||||
{
|
||||
Unloaded = vp.LoadRestriction_Unloaded,
|
||||
Loaded = vp.LoadRestriction_Loaded,
|
||||
LoadSetNames = SafeDeserializeLoadSetNames(vp.LoadRestriction_LoadSetNames)
|
||||
}
|
||||
: null,
|
||||
TrajectoryDegree = vp.TrajectoryDegree,
|
||||
TrajectoryControlPoint1X = vp.TrajectoryControlPoint1X,
|
||||
TrajectoryControlPoint1Y = vp.TrajectoryControlPoint1Y,
|
||||
TrajectoryControlPoint2X = vp.TrajectoryControlPoint2X,
|
||||
TrajectoryControlPoint2Y = vp.TrajectoryControlPoint2Y,
|
||||
CorridorLeftWidth = vp.CorridorLeftWidth,
|
||||
CorridorRightWidth = vp.CorridorRightWidth,
|
||||
CorridorRefPoint = vp.CorridorRefPoint
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string>? SafeDeserializeLoadSetNames(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try { return System.Text.Json.JsonSerializer.Deserialize<List<string>>(json); }
|
||||
catch (System.Text.Json.JsonException) { return null; }
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing background images for layout levels
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/images")]
|
||||
[Authorize]
|
||||
public class ImagesController(
|
||||
IImageStorageService imageStorageService,
|
||||
ILogger<ImagesController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IImageStorageService _imageStorageService = imageStorageService;
|
||||
private readonly ILogger<ImagesController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get background image for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>Image file (PNG)</returns>
|
||||
[HttpGet("layout/{layoutLevelId}")]
|
||||
[ProducesResponseType(typeof(FileStreamResult), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetLayoutImage(Guid layoutLevelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageStream = await _imageStorageService.GetImageAsync(layoutLevelId);
|
||||
|
||||
if (imageStream == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for layout level {LevelId}", layoutLevelId);
|
||||
return NotFound(CreateErrorResponse($"Image not found for layout level '{layoutLevelId}'", "IMAGE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return File(imageStream, "image/png", $"{layoutLevelId}.png");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Error retrieving image for layout level {LevelId}", layoutLevelId);
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while retrieving image", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload or replace background image for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <param name="file">Image file (PNG format)</param>
|
||||
/// <returns>Success message</returns>
|
||||
[HttpPost("layout/{layoutLevelId}")]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<IActionResult> UploadLayoutImage(Guid layoutLevelId, IFormFile file)
|
||||
{
|
||||
// Validate file
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(CreateErrorResponse("No file provided", "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; // 10MB
|
||||
if (file.Length > maxFileSize)
|
||||
{
|
||||
return BadRequest(CreateErrorResponse($"File size exceeds maximum of {maxFileSize / 1024 / 1024}MB", "VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await _imageStorageService.SaveImageAsync(layoutLevelId, stream);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
message = "Image uploaded successfully",
|
||||
layoutLevelId,
|
||||
fileName = $"{layoutLevelId}.png",
|
||||
size = file.Length
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Error uploading image for layout level {LevelId}", layoutLevelId);
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while uploading image", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete background image for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("layout/{layoutLevelId}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteLayoutImage(Guid layoutLevelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _imageStorageService.DeleteImageAsync(layoutLevelId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for deletion: {LevelId}", layoutLevelId);
|
||||
return NotFound(CreateErrorResponse($"Image not found for layout level '{layoutLevelId}'", "IMAGE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Error deleting image for layout level {LevelId}", layoutLevelId);
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while deleting image", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.LayoutData;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for retrieving complete layout data and merge/split operations
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/layout-data")]
|
||||
[Authorize]
|
||||
public class LayoutDataController(
|
||||
ILayoutDataService layoutDataService,
|
||||
ILogger<LayoutDataController> logger) : ControllerBase
|
||||
{
|
||||
private readonly ILayoutDataService _layoutDataService = layoutDataService;
|
||||
private readonly ILogger<LayoutDataController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get complete layout data for a layout level
|
||||
/// Returns all nodes, edges, and stations with full nested properties
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>Complete layout data</returns>
|
||||
[HttpGet("{layoutLevelId}")]
|
||||
[ProducesResponseType(typeof(LayoutDataDto), 200)]
|
||||
public async Task<ActionResult<LayoutDataDto>> GetLayoutData(Guid layoutLevelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _layoutDataService.GetLayoutDataAsync(layoutLevelId);
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Error retrieving layout data for level {LevelId}", layoutLevelId);
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while retrieving layout data", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple nodes into one node at center position
|
||||
/// </summary>
|
||||
/// <param name="request">Merge nodes request</param>
|
||||
/// <returns>Merge result with merged node, updated edges, and deleted node IDs</returns>
|
||||
[HttpPost("merge-nodes")]
|
||||
[ProducesResponseType(typeof(MergeNodesResponse), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<MergeNodesResponse>> MergeNodes([FromBody] MergeNodesRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _layoutDataService.MergeNodesAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to merge nodes");
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error merging nodes");
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while merging nodes", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split a node into multiple nodes (one for each connected edge)
|
||||
/// </summary>
|
||||
/// <param name="request">Split node request</param>
|
||||
/// <returns>Split result with new nodes, updated edges, and deleted node ID</returns>
|
||||
[HttpPost("split-node")]
|
||||
[ProducesResponseType(typeof(SplitNodeResponse), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<SplitNodeResponse>> SplitNode([FromBody] SplitNodeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _layoutDataService.SplitNodeAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to split node");
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error splitting node");
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while splitting node", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save all layout changes (nodes and edges) in a batch operation
|
||||
/// Uses transaction to ensure atomicity
|
||||
/// </summary>
|
||||
/// <param name="request">Save request with nodes and edges to update</param>
|
||||
/// <returns>Save result with counts and any skipped items</returns>
|
||||
[HttpPost("save")]
|
||||
[ProducesResponseType(typeof(SaveLayoutDataResponse), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<SaveLayoutDataResponse>> SaveLayoutData([FromBody] SaveLayoutDataRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _layoutDataService.SaveLayoutDataAsync(request);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving layout data");
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while saving layout data", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy selected nodes and edges with an offset
|
||||
/// Creates new nodes and edges at offset positions
|
||||
/// </summary>
|
||||
/// <param name="request">Copy request with node IDs, edge IDs, and offset</param>
|
||||
/// <returns>Copy result with newly created nodes and edges</returns>
|
||||
[HttpPost("copy-nodes")]
|
||||
[ProducesResponseType(typeof(CopyNodesResponse), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<CopyNodesResponse>> CopyNodes([FromBody] CopyNodesRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _layoutDataService.CopyNodesAsync(request);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error copying nodes");
|
||||
return StatusCode(500, CreateErrorResponse("Internal server error while copying nodes", "INTERNAL_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing nodes
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/nodes")]
|
||||
[Authorize]
|
||||
public class NodesController(
|
||||
INodeService nodeService,
|
||||
ILogger<NodesController> logger) : ControllerBase
|
||||
{
|
||||
private readonly INodeService _nodeService = nodeService;
|
||||
private readonly ILogger<NodesController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get all nodes for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>List of nodes with vehicle properties</returns>
|
||||
[HttpGet("level/{layoutLevelId}")]
|
||||
[ProducesResponseType(typeof(List<NodeDto>), 200)]
|
||||
public async Task<ActionResult<List<NodeDto>>> GetNodesByLevel(Guid layoutLevelId)
|
||||
{
|
||||
var nodes = await _nodeService.GetNodesByLevelAsync(layoutLevelId, includeVehicleProperties: true);
|
||||
var dtos = nodes.Select(MapToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get node by ID
|
||||
/// </summary>
|
||||
/// <param name="nodeId">Node database ID</param>
|
||||
/// <returns>Node details with vehicle properties</returns>
|
||||
[HttpGet("{nodeId}")]
|
||||
[ProducesResponseType(typeof(NodeDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<NodeDto>> GetNode(Guid nodeId)
|
||||
{
|
||||
var node = await _nodeService.GetByIdAsync(nodeId, includeVehicleProperties: true);
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Node with ID '{nodeId}' not found", "NODE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapToDto(node));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update node
|
||||
/// </summary>
|
||||
/// <param name="nodeId">Node database ID</param>
|
||||
/// <param name="request">Update request</param>
|
||||
/// <returns>Updated node</returns>
|
||||
[HttpPut("{nodeId}")]
|
||||
[ProducesResponseType(typeof(NodeDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<NodeDto>> UpdateNode(
|
||||
Guid nodeId,
|
||||
[FromBody] UpdateNodeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var node = await _nodeService.UpdateAsync(nodeId, request);
|
||||
|
||||
return Ok(MapToDto(node));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update node: {NodeId}", nodeId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "NODE_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to map entity to DTO
|
||||
private static NodeDto MapToDto(Data.Node node)
|
||||
{
|
||||
return new NodeDto
|
||||
{
|
||||
Id = node.Id,
|
||||
LevelId = node.LevelId,
|
||||
NodeId = node.NodeId,
|
||||
NodeName = node.NodeName,
|
||||
NodeDescription = node.NodeDescription,
|
||||
MapId = node.MapId,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
VehicleProperties = node.VehicleProperties?.Select(vp => new NodeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
NodeId = vp.NodeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
VehicleTypeIdString = vp.VehicleType?.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions,
|
||||
AllowedDeviationXY = vp.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = vp.AllowedDeviationTheta
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Station;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing stations
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/stations")]
|
||||
[Authorize]
|
||||
public class StationsController(
|
||||
IStationService stationService,
|
||||
ILogger<StationsController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IStationService _stationService = stationService;
|
||||
private readonly ILogger<StationsController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new station
|
||||
/// </summary>
|
||||
/// <param name="request">Station creation request</param>
|
||||
/// <returns>Created station</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(StationDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<StationDto>> CreateStation([FromBody] CreateStationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var station = await _stationService.CreateAsync(request);
|
||||
var dto = MapToDto(station);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetStation),
|
||||
new { stationId = station.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create station: {StationId}", request.StationId);
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all stations for a layout level
|
||||
/// </summary>
|
||||
/// <param name="layoutLevelId">Layout level ID</param>
|
||||
/// <returns>List of stations with interaction nodes</returns>
|
||||
[HttpGet("level/{layoutLevelId}")]
|
||||
[ProducesResponseType(typeof(List<StationDto>), 200)]
|
||||
public async Task<ActionResult<List<StationDto>>> GetStationsByLevel(Guid layoutLevelId)
|
||||
{
|
||||
var stations = await _stationService.GetStationsByLevelAsync(layoutLevelId, includeInteractionNodes: true);
|
||||
var dtos = stations.Select(MapToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get station by ID
|
||||
/// </summary>
|
||||
/// <param name="stationId">Station database ID</param>
|
||||
/// <returns>Station details with interaction nodes</returns>
|
||||
[HttpGet("{stationId}")]
|
||||
[ProducesResponseType(typeof(StationDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<StationDto>> GetStation(Guid stationId)
|
||||
{
|
||||
var station = await _stationService.GetByIdAsync(stationId, includeInteractionNodes: true);
|
||||
|
||||
if (station == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Station with ID '{stationId}' not found", "STATION_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapToDto(station));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update station
|
||||
/// </summary>
|
||||
/// <param name="stationId">Station database ID</param>
|
||||
/// <param name="request">Update request</param>
|
||||
/// <returns>Updated station</returns>
|
||||
[HttpPut("{stationId}")]
|
||||
[ProducesResponseType(typeof(StationDto), 200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<StationDto>> UpdateStation(
|
||||
Guid stationId,
|
||||
[FromBody] UpdateStationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var station = await _stationService.UpdateAsync(stationId, request);
|
||||
|
||||
return Ok(MapToDto(station));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update station: {StationId}", stationId);
|
||||
|
||||
if (ex.Message.Contains("not found"))
|
||||
return NotFound(CreateErrorResponse(ex.Message, "STATION_NOT_FOUND"));
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete station
|
||||
/// Deletes station and cascade deletes interaction nodes (but NOT the linked nodes)
|
||||
/// </summary>
|
||||
/// <param name="stationId">Station database ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("{stationId}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> DeleteStation(Guid stationId)
|
||||
{
|
||||
var deleted = await _stationService.DeleteAsync(stationId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse($"Station with ID '{stationId}' not found", "STATION_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// Helper method to map entity to DTO
|
||||
private static StationDto MapToDto(Data.Station station)
|
||||
{
|
||||
return new StationDto
|
||||
{
|
||||
Id = station.Id,
|
||||
LevelId = station.LevelId,
|
||||
StationId = station.StationId,
|
||||
StationName = station.StationName,
|
||||
StationDescription = station.StationDescription,
|
||||
StationHeight = station.StationHeight,
|
||||
X = station.X,
|
||||
Y = station.Y,
|
||||
Theta = station.Theta,
|
||||
InteractionNodes = station.InteractionNodes?.Select(sin => new StationInteractionNodeDto
|
||||
{
|
||||
Id = sin.Id,
|
||||
StationId = sin.StationId,
|
||||
NodeId = sin.NodeId,
|
||||
Node = sin.Node != null ? new NodeDto
|
||||
{
|
||||
Id = sin.Node.Id,
|
||||
NodeId = sin.Node.NodeId,
|
||||
NodeName = sin.Node.NodeName,
|
||||
X = sin.Node.X,
|
||||
Y = sin.Node.Y,
|
||||
VehicleProperties = sin.Node.VehicleProperties?.Select(vp => new NodeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions
|
||||
}).ToList()
|
||||
} : null
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static ErrorResponseDto CreateErrorResponse(
|
||||
string error,
|
||||
string? errorCode = null,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new ErrorResponseDto
|
||||
{
|
||||
Error = error,
|
||||
ErrorCode = errorCode,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.VehicleType;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.MapManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing vehicle types
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/vehicles")]
|
||||
[Authorize]
|
||||
public class VehiclesManagerController(
|
||||
IVehicleTypeService vehicleTypeService,
|
||||
ILogger<VehiclesManagerController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IVehicleTypeService _vehicleTypeService = vehicleTypeService;
|
||||
private readonly ILogger<VehiclesManagerController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new vehicle type
|
||||
/// </summary>
|
||||
/// <param name="request">Vehicle type creation request</param>
|
||||
/// <returns>Created vehicle type</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(VehicleTypeDto), 201)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<ActionResult<VehicleTypeDto>> CreateVehicleType([FromBody] CreateVehicleTypeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vehicleType = await _vehicleTypeService.CreateAsync(
|
||||
request.VehicleTypeId,
|
||||
request.VehicleTypeName,
|
||||
request.Description,
|
||||
request.Specifications,
|
||||
request.Actions);
|
||||
|
||||
var dto = MapToDto(vehicleType);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetVehicleType),
|
||||
new { vehicleTypeId = vehicleType.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create vehicle type: {VehicleTypeId}", request.VehicleTypeId);
|
||||
var errorCode = ex.Message.Contains("already exists")
|
||||
? "VEHICLE_TYPE_ALREADY_EXISTS"
|
||||
: "VALIDATION_ERROR";
|
||||
return BadRequest(CreateErrorResponse(ex.Message, errorCode));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all vehicle types
|
||||
/// </summary>
|
||||
/// <param name="isActive">Optional filter by active status</param>
|
||||
/// <returns>List of vehicle types</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<VehicleTypeDto>), 200)]
|
||||
public async Task<ActionResult<List<VehicleTypeDto>>> GetAllVehicleTypes([FromQuery] bool? isActive)
|
||||
{
|
||||
List<Data.VehicleType> vehicleTypes;
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
vehicleTypes = await _vehicleTypeService.GetByActiveStatusAsync(isActive.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
vehicleTypes = await _vehicleTypeService.GetAllAsync();
|
||||
}
|
||||
|
||||
var dtos = vehicleTypes.Select(MapToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get vehicle type by database ID
|
||||
/// </summary>
|
||||
/// <param name="vehicleTypeId">Vehicle type database ID</param>
|
||||
/// <returns>Vehicle type details</returns>
|
||||
[HttpGet("{vehicleTypeId}")]
|
||||
[ProducesResponseType(typeof(VehicleTypeDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<VehicleTypeDto>> GetVehicleType(Guid vehicleTypeId)
|
||||
{
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId);
|
||||
|
||||
if (vehicleType == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse(
|
||||
$"Vehicle type with ID '{vehicleTypeId}' not found",
|
||||
"VEHICLE_TYPE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapToDto(vehicleType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get vehicle type by VehicleTypeId string
|
||||
/// </summary>
|
||||
/// <param name="vehicleTypeId">Vehicle type identifier string (e.g., "AMR-T800")</param>
|
||||
/// <returns>Vehicle type details</returns>
|
||||
[HttpGet("vehicleTypeId/{vehicleTypeId}")]
|
||||
[ProducesResponseType(typeof(VehicleTypeDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<VehicleTypeDto>> GetVehicleTypeByStringId(string vehicleTypeId)
|
||||
{
|
||||
var vehicleType = await _vehicleTypeService.GetByVehicleTypeIdAsync(vehicleTypeId);
|
||||
|
||||
if (vehicleType == null)
|
||||
{
|
||||
return NotFound(CreateErrorResponse(
|
||||
$"Vehicle type with VehicleTypeId '{vehicleTypeId}' not found",
|
||||
"VEHICLE_TYPE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return Ok(MapToDto(vehicleType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search vehicle types by query string
|
||||
/// Searches in VehicleTypeId and VehicleTypeName (case-insensitive)
|
||||
/// </summary>
|
||||
/// <param name="query">Search query</param>
|
||||
/// <returns>List of matching vehicle types</returns>
|
||||
[HttpGet("search")]
|
||||
[ProducesResponseType(typeof(List<VehicleTypeDto>), 200)]
|
||||
public async Task<ActionResult<List<VehicleTypeDto>>> SearchVehicleTypes([FromQuery] string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return BadRequest(CreateErrorResponse(
|
||||
"Query parameter is required",
|
||||
"VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
var vehicleTypes = await _vehicleTypeService.SearchAsync(query);
|
||||
var dtos = vehicleTypes.Select(MapToDto).ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get usage information for a vehicle type
|
||||
/// </summary>
|
||||
/// <param name="id">Vehicle type database ID</param>
|
||||
/// <returns>Usage information</returns>
|
||||
[HttpGet("{id}/usage")]
|
||||
[ProducesResponseType(typeof(VehicleTypeUsageInfoDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<VehicleTypeUsageInfoDto>> GetVehicleTypeUsage(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var usageInfo = await _vehicleTypeService.GetUsageInfoAsync(id);
|
||||
var dto = MapUsageInfoToDto(usageInfo);
|
||||
|
||||
return Ok(dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get usage info for vehicle type: {Id}", id);
|
||||
return NotFound(CreateErrorResponse(ex.Message, "VEHICLE_TYPE_NOT_FOUND"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update vehicle type
|
||||
/// </summary>
|
||||
/// <param name="vehicleTypeId">Vehicle type database ID</param>
|
||||
/// <param name="request">Update request</param>
|
||||
/// <returns>Updated vehicle type</returns>
|
||||
[HttpPut("{vehicleTypeId}")]
|
||||
[ProducesResponseType(typeof(VehicleTypeDto), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<VehicleTypeDto>> UpdateVehicleType(
|
||||
Guid vehicleTypeId,
|
||||
[FromBody] UpdateVehicleTypeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vehicleType = await _vehicleTypeService.UpdateAsync(
|
||||
vehicleTypeId,
|
||||
request.VehicleTypeName,
|
||||
request.Description,
|
||||
request.Specifications,
|
||||
request.Actions,
|
||||
request.IsActive);
|
||||
|
||||
return Ok(MapToDto(vehicleType));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to update vehicle type: {VehicleTypeId}", vehicleTypeId);
|
||||
return NotFound(CreateErrorResponse(ex.Message, "VEHICLE_TYPE_NOT_FOUND"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete vehicle type
|
||||
/// </summary>
|
||||
/// <param name="vehicleTypeId">Vehicle type database ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("{vehicleTypeId}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
[ProducesResponseType(400)]
|
||||
public async Task<IActionResult> DeleteVehicleType(Guid vehicleTypeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _vehicleTypeService.DeleteAsync(vehicleTypeId);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(CreateErrorResponse(
|
||||
$"Vehicle type with ID '{vehicleTypeId}' not found",
|
||||
"VEHICLE_TYPE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning(ex, "Failed to delete vehicle type: {VehicleTypeId}", vehicleTypeId);
|
||||
|
||||
var errorCode = ex.Message.Contains("referenced")
|
||||
? "VEHICLE_TYPE_IN_USE"
|
||||
: "VALIDATION_ERROR";
|
||||
|
||||
var details = new Dictionary<string, object>();
|
||||
if (errorCode == "VEHICLE_TYPE_IN_USE")
|
||||
{
|
||||
// Try to get usage info for details
|
||||
try
|
||||
{
|
||||
var usageInfo = await _vehicleTypeService.GetUsageInfoAsync(vehicleTypeId);
|
||||
details["nodePropertiesCount"] = usageInfo.NodePropertiesCount;
|
||||
details["edgePropertiesCount"] = usageInfo.EdgePropertiesCount;
|
||||
details["totalUsageCount"] = usageInfo.TotalUsageCount;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore if we can't get usage info
|
||||
}
|
||||
}
|
||||
|
||||
return BadRequest(CreateErrorResponse(ex.Message, errorCode, details));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to map entity to DTO
|
||||
private static VehicleTypeDto MapToDto(Data.VehicleType vehicleType)
|
||||
{
|
||||
return new VehicleTypeDto
|
||||
{
|
||||
Id = vehicleType.Id,
|
||||
VehicleTypeId = vehicleType.VehicleTypeId,
|
||||
VehicleTypeName = vehicleType.VehicleTypeName,
|
||||
Description = vehicleType.Description,
|
||||
Specifications = vehicleType.Specifications,
|
||||
Actions = vehicleType.Actions,
|
||||
IsActive = vehicleType.IsActive,
|
||||
CreatedDate = vehicleType.CreatedDate
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to map usage info to DTO
|
||||
private static VehicleTypeUsageInfoDto MapUsageInfoToDto(VehicleTypeUsageInfo usageInfo)
|
||||
{
|
||||
return new VehicleTypeUsageInfoDto
|
||||
{
|
||||
VehicleTypeId = usageInfo.VehicleTypeId,
|
||||
VehicleTypeIdString = usageInfo.VehicleTypeIdString,
|
||||
VehicleTypeName = usageInfo.VehicleTypeName,
|
||||
NodePropertiesCount = usageInfo.NodePropertiesCount,
|
||||
EdgePropertiesCount = usageInfo.EdgePropertiesCount,
|
||||
TotalUsageCount = usageInfo.TotalUsageCount,
|
||||
CanDelete = usageInfo.CanDelete
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to create error response
|
||||
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