Files
BQP/srcs/RobotNet10/Commons/RobotNet10.MapManager/Services/LayoutService.cs
2026-07-13 09:25:40 +07:00

401 lines
14 KiB
C#

using Microsoft.EntityFrameworkCore;
using RobotNet10.MapEditor.Shared.DTOs.Requests;
using RobotNet10.MapManager.Data;
namespace RobotNet10.MapManager.Services;
/// <summary>
/// Service implementation for managing layouts, versions, and levels
/// </summary>
public class LayoutService(MapDbContext context) : ILayoutService
{
private readonly MapDbContext _context = context;
// ==========================================
// LAYOUT OPERATIONS
// ==========================================
public async Task<Layout> CreateLayoutAsync(CreateLayoutRequest request)
{
// Check if LayoutId already exists
var exists = await _context.Layouts.AnyAsync(l => l.LayoutId == request.LayoutId);
if (exists)
{
throw new InvalidOperationException($"Layout with ID '{request.LayoutId}' already exists");
}
var layout = new Layout
{
LayoutId = request.LayoutId,
LayoutName = request.LayoutName,
Description = request.Description,
IsActive = false,
CreatedDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow,
CreatedBy = request.CreatedBy
};
_context.Layouts.Add(layout);
await _context.SaveChangesAsync();
return layout;
}
public async Task<List<Layout>> SearchLayoutsAsync(string? searchText)
{
var query = _context.Layouts.Include(l => l.Versions)
.ThenInclude(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.AsSplitQuery()
.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchText))
{
var search = searchText.ToLower();
query = query.Where(l =>
l.LayoutId.ToLower().Contains(search) ||
l.LayoutName.ToLower().Contains(search));
}
return await query
.OrderByDescending(l => l.ModifiedDate)
.ToListAsync();
}
public async Task<Layout?> GetLayoutByIdAsync(Guid layoutId)
{
return await _context.Layouts
.Include(l => l.Versions)
.ThenInclude(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.FirstOrDefaultAsync(l => l.Id == layoutId);
}
public async Task<Layout?> GetLayoutByLayoutIdAsync(string layoutId)
{
return await _context.Layouts
.Include(l => l.Versions)
.ThenInclude(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.FirstOrDefaultAsync(l => l.LayoutId == layoutId);
}
public async Task<Layout?> GetLayoutByNameAsync(string layoutName)
{
return await _context.Layouts
.Include(l => l.Versions)
.ThenInclude(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.FirstOrDefaultAsync(l => l.LayoutName == layoutName);
}
public async Task<Layout> UpdateLayoutAsync(Guid layoutId, UpdateLayoutRequest request)
{
var layout = await GetLayoutByIdAsync(layoutId) ??
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
layout.LayoutName = request.LayoutName;
layout.Description = request.Description;
layout.ModifiedDate = DateTime.UtcNow;
layout.ModifiedBy = request.ModifiedBy;
await _context.SaveChangesAsync();
return layout;
}
public async Task<bool> DeleteLayoutAsync(Guid layoutId)
{
var layout = await GetLayoutByIdAsync(layoutId);
if (layout == null)
{
return false;
}
// Check if layout is active
if (layout.IsActive)
{
throw new InvalidOperationException(
$"Cannot delete active layout '{layout.LayoutId}'. Deactivate it first.");
}
// Hard delete - EF Core cascade will handle:
// Layout → Versions → Levels → Nodes/Edges/Stations → Properties
_context.Layouts.Remove(layout);
await _context.SaveChangesAsync();
return true;
}
public async Task<Layout> ActivateLayoutAsync(Guid layoutId)
{
var layout = await GetLayoutByIdAsync(layoutId) ??
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
layout.IsActive = true;
layout.ModifiedDate = DateTime.UtcNow;
await _context.SaveChangesAsync();
return layout;
}
public async Task<Layout> DeactivateLayoutAsync(Guid layoutId)
{
var layout = await GetLayoutByIdAsync(layoutId) ??
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
layout.IsActive = false;
layout.ModifiedDate = DateTime.UtcNow;
await _context.SaveChangesAsync();
return layout;
}
// ==========================================
// VERSION OPERATIONS
// ==========================================
public async Task<LayoutVersion> CreateVersionAsync(Guid layoutId, CreateLayoutVersionRequest request)
{
var layout = await GetLayoutByIdAsync(layoutId) ??
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
// Check if version already exists
var exists = await _context.LayoutVersions
.AnyAsync(v => v.LayoutId == layoutId && v.Version == request.Version);
if (exists)
{
throw new InvalidOperationException(
$"Version '{request.Version}' already exists for layout '{layout.LayoutId}'");
}
var version = new LayoutVersion
{
LayoutId = layoutId,
Version = request.Version,
LayoutDescription = request.LayoutDescription,
CreatedBy = request.CreatedBy,
CreatedDate = DateTime.UtcNow,
IsActive = false // New versions start as inactive
};
_context.LayoutVersions.Add(version);
await _context.SaveChangesAsync();
return version;
}
public async Task<List<LayoutVersion>> GetVersionsAsync(Guid layoutId)
{
return await _context.LayoutVersions
.Where(v => v.LayoutId == layoutId)
.Include(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.OrderByDescending(v => v.CreatedDate)
.ToListAsync();
}
public async Task<LayoutVersion?> GetVersionAsync(Guid versionId)
{
return await _context.LayoutVersions
.Include(v => v.Layout)
.Include(v => v.Levels)
.ThenInclude(l => l.EditorSettings)
.FirstOrDefaultAsync(v => v.Id == versionId);
}
public async Task<LayoutVersion> UpdateVersionAsync(Guid versionId, UpdateLayoutRequest request)
{
var version = await GetVersionAsync(versionId) ??
throw new InvalidOperationException($"Version with ID '{versionId}' not found");
version.LayoutDescription = request.Description;
await _context.SaveChangesAsync();
return version;
}
public async Task<bool> DeleteVersionAsync(Guid versionId)
{
var version = await GetVersionAsync(versionId);
if (version == null)
{
return false;
}
// Check if parent layout is active
if (version.Layout.IsActive)
{
throw new InvalidOperationException(
$"Cannot delete version from active layout '{version.Layout.LayoutId}'. " +
"Deactivate the layout first.");
}
// Hard delete - cascade will handle levels and all nested data
_context.LayoutVersions.Remove(version);
await _context.SaveChangesAsync();
return true;
}
// ==========================================
// LEVEL OPERATIONS
// ==========================================
public async Task<LayoutLevel> CreateLevelAsync(Guid versionId, CreateLayoutLevelRequest request)
{
var version = await GetVersionAsync(versionId) ??
throw new InvalidOperationException($"Version with ID '{versionId}' not found");
// Check if level already exists
var exists = await _context.LayoutLevels
.AnyAsync(l => l.VersionId == versionId && l.LayoutLevelId == request.LayoutLevelId);
if (exists)
{
throw new InvalidOperationException(
$"Level '{request.LayoutLevelId}' already exists in this version");
}
var level = new LayoutLevel
{
VersionId = versionId,
LayoutLevelId = request.LayoutLevelId,
LevelOrder = request.LevelOrder
};
_context.LayoutLevels.Add(level);
await _context.SaveChangesAsync();
// Create editor settings if coordinate system provided
if (request.CoordinateSystem != null)
{
var settings = new LayoutLevelEditorSettings
{
LevelId = level.Id,
OriginX = request.CoordinateSystem.OriginX,
OriginY = request.CoordinateSystem.OriginY,
Resolution = request.CoordinateSystem.Resolution,
BoundsMinX = request.CoordinateSystem.BoundsMinX,
BoundsMaxX = request.CoordinateSystem.BoundsMaxX,
BoundsMinY = request.CoordinateSystem.BoundsMinY,
BoundsMaxY = request.CoordinateSystem.BoundsMaxY,
ImageWidth = request.CoordinateSystem.ImageWidth,
ImageHeight = request.CoordinateSystem.ImageHeight,
CreatedDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow
};
_context.LayoutLevelEditorSettings.Add(settings);
await _context.SaveChangesAsync();
}
return level;
}
public async Task<List<LayoutLevel>> GetLevelsAsync(Guid versionId)
{
return await _context.LayoutLevels
.Where(l => l.VersionId == versionId)
.Include(l => l.EditorSettings)
.OrderBy(l => l.LevelOrder)
.ToListAsync();
}
public async Task<LayoutLevel?> GetLevelAsync(Guid levelId)
{
return await _context.LayoutLevels
.Include(l => l.Version)
.ThenInclude(v => v.Layout)
.Include(l => l.EditorSettings)
.FirstOrDefaultAsync(l => l.Id == levelId);
}
public async Task<LayoutLevel> UpdateLevelAsync(Guid levelId, UpdateLayoutLevelRequest request)
{
var level = await GetLevelAsync(levelId) ?? throw new InvalidOperationException($"Level with ID '{levelId}' not found");
// Update level properties
if (request.LayoutLevelId != null)
level.LayoutLevelId = request.LayoutLevelId;
if (request.LevelOrder.HasValue)
level.LevelOrder = request.LevelOrder.Value;
// Get or create editor settings
var settings = await _context.LayoutLevelEditorSettings
.FirstOrDefaultAsync(s => s.LevelId == levelId);
if (settings == null)
{
// Create new settings
settings = new LayoutLevelEditorSettings
{
LevelId = levelId,
CreatedDate = DateTime.UtcNow
};
_context.LayoutLevelEditorSettings.Add(settings);
}
// Update coordinate system if provided
if (request.CoordinateSystem != null)
{
settings.OriginX = request.CoordinateSystem.OriginX;
settings.OriginY = request.CoordinateSystem.OriginY;
settings.Resolution = request.CoordinateSystem.Resolution;
settings.BoundsMinX = request.CoordinateSystem.BoundsMinX;
settings.BoundsMaxX = request.CoordinateSystem.BoundsMaxX;
settings.BoundsMinY = request.CoordinateSystem.BoundsMinY;
settings.BoundsMaxY = request.CoordinateSystem.BoundsMaxY;
settings.ImageWidth = request.CoordinateSystem.ImageWidth;
settings.ImageHeight = request.CoordinateSystem.ImageHeight;
}
// Update editor settings if provided
if (request.EditorSettings != null)
{
if (request.EditorSettings.EdgeMinLengthCreate.HasValue)
settings.EdgeMinLengthCreate = request.EditorSettings.EdgeMinLengthCreate.Value;
if (request.EditorSettings.EdgeNameAutoGenerate.HasValue)
settings.EdgeNameAutoGenerate = request.EditorSettings.EdgeNameAutoGenerate.Value;
if (request.EditorSettings.NodeNameAutoGenerate.HasValue)
settings.NodeNameAutoGenerate = request.EditorSettings.NodeNameAutoGenerate.Value;
if (request.EditorSettings.NodeProximityRadius.HasValue)
settings.NodeProximityRadius = request.EditorSettings.NodeProximityRadius.Value;
}
// Update modified date if any settings were changed
if (request.CoordinateSystem != null || request.EditorSettings != null)
{
settings.ModifiedDate = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
return level;
}
public async Task<bool> DeleteLevelAsync(Guid levelId)
{
var level = await GetLevelAsync(levelId);
if (level == null)
{
return false;
}
// Check if parent layout is active
if (level.Version.Layout.IsActive)
{
throw new InvalidOperationException(
$"Cannot delete level from active layout '{level.Version.Layout.LayoutId}'. " +
"Deactivate the layout first.");
}
// Hard delete - cascade will handle all nested data
_context.LayoutLevels.Remove(level);
await _context.SaveChangesAsync();
return true;
}
}