diff --git a/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaAdd.razor b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaAdd.razor new file mode 100644 index 0000000..30d74ec --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaAdd.razor @@ -0,0 +1,127 @@ +@page "/admin/areas/add" +@using ScadaLink.Security +@using ScadaLink.Commons.Entities.Instances +@using ScadaLink.Commons.Entities.Sites +@using ScadaLink.Commons.Interfaces.Repositories +@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)] +@inject ITemplateEngineRepository TemplateEngineRepository +@inject ISiteRepository SiteRepository +@inject NavigationManager NavigationManager + +
+
+ ← Back +

Add Area

+
+ + + + @if (_loading) + { + + } + else if (_errorMessage != null) + { +
@_errorMessage
+ } + else + { +
+
+
+ + +
+
+ + +
+
+ + +
+ @if (_formError != null) + { +
@_formError
+ } + +
+
+ } +
+ +@code { + [SupplyParameterFromQuery] public int SiteId { get; set; } + [SupplyParameterFromQuery] public int ParentAreaId { get; set; } + + private string _siteName = string.Empty; + private List _areas = new(); + private int _parentAreaId; + private string _name = string.Empty; + private string? _formError; + private string? _errorMessage; + private bool _loading = true; + private bool _saving; + + private ToastNotification _toast = default!; + + protected override async Task OnInitializedAsync() + { + try + { + var site = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.Id == SiteId); + _siteName = site?.Name ?? $"Site #{SiteId}"; + _areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(SiteId)).ToList(); + _parentAreaId = ParentAreaId; + } + catch (Exception ex) + { + _errorMessage = $"Failed to load: {ex.Message}"; + } + _loading = false; + } + + private string GetAreaPath(Area area) + { + var parts = new List(); + var current = area; + while (current != null) + { + parts.Insert(0, current.Name); + current = current.ParentAreaId.HasValue + ? _areas.FirstOrDefault(a => a.Id == current.ParentAreaId.Value) + : null; + } + return string.Join(" / ", parts); + } + + private async Task Save() + { + _formError = null; + if (string.IsNullOrWhiteSpace(_name)) { _formError = "Name is required."; return; } + + _saving = true; + try + { + var area = new Area(_name.Trim()) + { + SiteId = SiteId, + ParentAreaId = _parentAreaId == 0 ? null : _parentAreaId + }; + await TemplateEngineRepository.AddAreaAsync(area); + await TemplateEngineRepository.SaveChangesAsync(); + NavigationManager.NavigateTo("/admin/areas"); + } + catch (Exception ex) + { + _formError = $"Save failed: {ex.Message}"; + } + _saving = false; + } +} diff --git a/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaDelete.razor b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaDelete.razor new file mode 100644 index 0000000..df083ae --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaDelete.razor @@ -0,0 +1,199 @@ +@page "/admin/areas/{Id:int}/delete" +@using ScadaLink.Security +@using ScadaLink.Commons.Entities.Instances +@using ScadaLink.Commons.Entities.Sites +@using ScadaLink.Commons.Interfaces.Repositories +@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)] +@inject ITemplateEngineRepository TemplateEngineRepository +@inject ISiteRepository SiteRepository +@inject NavigationManager NavigationManager + +
+
+ ← Back +

Delete Area

+
+ + + + + @if (_loading) + { + + } + else if (_errorMessage != null) + { +
@_errorMessage
+ } + else + { +
+
+

+ You are about to delete area @_area!.Name. + @if (_hasBlockingInstances) + { + This area (or its children) has instances assigned. Remove or reassign instances before deleting. + } +

+ +
Area hierarchy with assigned instances:
+ + + + @if (node.Kind == DeleteNodeKind.Area) + { + @node.Label + @if (node.HasInstances) + { + @node.InstanceCount instance(s) + } + } + else + { + @node.Label + } + + + No child areas. + + + +
+ @if (_hasBlockingInstances) + { + + } + else + { + + } + Cancel +
+
+
+ } +
+ +@code { + [Parameter] public int Id { get; set; } + + record DeleteTreeNode(string Key, string Label, DeleteNodeKind Kind, List Children, + bool HasInstances = false, int InstanceCount = 0); + enum DeleteNodeKind { Area, Instance } + + private Area? _area; + private List _treeRoots = new(); + private bool _hasBlockingInstances; + private bool _loading = true; + private bool _deleting; + private string? _errorMessage; + + private ToastNotification _toast = default!; + private ConfirmDialog _confirmDialog = default!; + + protected override async Task OnInitializedAsync() + { + try + { + _area = await TemplateEngineRepository.GetAreaByIdAsync(Id); + if (_area == null) + { + _errorMessage = $"Area #{Id} not found."; + _loading = false; + return; + } + + // Load all areas for this site to build hierarchy + var allAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_area.SiteId)).ToList(); + var allInstances = (await TemplateEngineRepository.GetAllInstancesAsync()) + .Where(i => i.SiteId == _area.SiteId) + .ToList(); + + var rootNode = BuildDeleteTree(_area, allAreas, allInstances); + _treeRoots = new List { rootNode }; + _hasBlockingInstances = HasAnyInstances(rootNode); + } + catch (Exception ex) + { + _errorMessage = $"Failed to load: {ex.Message}"; + } + _loading = false; + } + + private DeleteTreeNode BuildDeleteTree(Area area, List allAreas, List allInstances) + { + var children = new List(); + + // Add child areas recursively + var childAreas = allAreas.Where(a => a.ParentAreaId == area.Id).OrderBy(a => a.Name); + foreach (var child in childAreas) + { + children.Add(BuildDeleteTree(child, allAreas, allInstances)); + } + + // Add instances assigned to this area + var areaInstances = allInstances.Where(i => i.AreaId == area.Id).OrderBy(i => i.UniqueName); + foreach (var inst in areaInstances) + { + children.Add(new DeleteTreeNode( + Key: $"inst-{inst.Id}", + Label: inst.UniqueName, + Kind: DeleteNodeKind.Instance, + Children: new())); + } + + var instanceCount = areaInstances.Count(); + return new DeleteTreeNode( + Key: $"area-{area.Id}", + Label: area.Name, + Kind: DeleteNodeKind.Area, + Children: children, + HasInstances: instanceCount > 0, + InstanceCount: instanceCount); + } + + private bool HasAnyInstances(DeleteTreeNode node) + { + if (node.Kind == DeleteNodeKind.Instance) return true; + return node.Children.Any(HasAnyInstances); + } + + private async Task Delete() + { + var confirmed = await _confirmDialog.ShowAsync( + $"Permanently delete area '{_area!.Name}' and all its child areas?", + "Confirm Delete"); + if (!confirmed) return; + + _deleting = true; + try + { + // Delete child areas bottom-up (deepest first) + await DeleteAreaRecursive(_area!); + await TemplateEngineRepository.SaveChangesAsync(); + NavigationManager.NavigateTo("/admin/areas"); + } + catch (Exception ex) + { + _toast.ShowError($"Delete failed: {ex.Message}"); + } + _deleting = false; + } + + private async Task DeleteAreaRecursive(Area area) + { + // Load fresh children in case the collection wasn't populated + var allAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(area.SiteId)).ToList(); + var children = allAreas.Where(a => a.ParentAreaId == area.Id).ToList(); + foreach (var child in children) + { + await DeleteAreaRecursive(child); + } + await TemplateEngineRepository.DeleteAreaAsync(area.Id); + } +} diff --git a/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaEdit.razor b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaEdit.razor new file mode 100644 index 0000000..cb189f5 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Admin/AreaEdit.razor @@ -0,0 +1,95 @@ +@page "/admin/areas/{Id:int}/edit" +@using ScadaLink.Security +@using ScadaLink.Commons.Entities.Instances +@using ScadaLink.Commons.Interfaces.Repositories +@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)] +@inject ITemplateEngineRepository TemplateEngineRepository +@inject NavigationManager NavigationManager + +
+
+ ← Back +

Edit Area

+
+ + + + @if (_loading) + { + + } + else if (_errorMessage != null) + { +
@_errorMessage
+ } + else + { +
+
+
+ + +
+ @if (_formError != null) + { +
@_formError
+ } + +
+
+ } +
+ +@code { + [Parameter] public int Id { get; set; } + + private Area? _area; + private string _name = string.Empty; + private string? _formError; + private string? _errorMessage; + private bool _loading = true; + private bool _saving; + + private ToastNotification _toast = default!; + + protected override async Task OnInitializedAsync() + { + try + { + _area = await TemplateEngineRepository.GetAreaByIdAsync(Id); + if (_area == null) + { + _errorMessage = $"Area #{Id} not found."; + } + else + { + _name = _area.Name; + } + } + catch (Exception ex) + { + _errorMessage = $"Failed to load area: {ex.Message}"; + } + _loading = false; + } + + private async Task Save() + { + _formError = null; + if (string.IsNullOrWhiteSpace(_name)) { _formError = "Name is required."; return; } + + _saving = true; + try + { + _area!.Name = _name.Trim(); + await TemplateEngineRepository.UpdateAreaAsync(_area); + await TemplateEngineRepository.SaveChangesAsync(); + NavigationManager.NavigateTo("/admin/areas"); + } + catch (Exception ex) + { + _formError = $"Save failed: {ex.Message}"; + } + _saving = false; + } +} diff --git a/src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor b/src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor index c617872..4a0a057 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor @@ -6,14 +6,12 @@ @attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)] @inject ISiteRepository SiteRepository @inject ITemplateEngineRepository TemplateEngineRepository +@inject NavigationManager NavigationManager
-
-

Area Management

-
+

Area Management

- @if (_loading) { @@ -25,246 +23,111 @@ } else { -
-
-
-
-
Sites
-
-
- @if (_sites.Count == 0) - { -
No sites configured.
- } - @foreach (var site in _sites) - { - - } -
-
-
- -
- @if (_selectedSiteId == 0) + + + @if (node.Kind == AreaNodeKind.Site) { -
Select a site to manage its areas.
+ @node.Label + @node.AreaCount } else { -
-
Areas for @(_sites.FirstOrDefault(s => s.Id == _selectedSiteId)?.Name)
- -
- - @if (_showForm) - { -
-
-
@(_editingArea == null ? "Add New Area" : "Edit Area")
-
-
- - -
- @if (_editingArea == null) - { -
- - -
- } -
- - -
-
- @if (_formError != null) - { -
@_formError
- } -
-
- } - - - - @area.Name - - - - - - - - No areas for this site. Add one above. - - + @node.Label } -
-
+ + + @if (node.Kind == AreaNodeKind.Site) + { + + } + else + { + + + + + } + + + No sites configured. + + }
@code { + record AreaTreeNode(string Key, string Label, AreaNodeKind Kind, List Children, + int SiteId, Area? Area = null, int AreaCount = 0); + enum AreaNodeKind { Site, Area } + private List _sites = new(); - private List _areas = new(); - private int _selectedSiteId; + private List _treeRoots = new(); private bool _loading = true; private string? _errorMessage; - private bool _showForm; - private Area? _editingArea; - private string _formName = string.Empty; - private int _formParentAreaId; - private string? _formError; - private ToastNotification _toast = default!; - private ConfirmDialog _confirmDialog = default!; protected override async Task OnInitializedAsync() + { + await LoadDataAsync(); + } + + private async Task LoadDataAsync() { _loading = true; try { _sites = (await SiteRepository.GetAllSitesAsync()).ToList(); + _treeRoots = new(); + foreach (var site in _sites) + { + var areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(site.Id)).ToList(); + var rootAreas = areas.Where(a => a.ParentAreaId == null).OrderBy(a => a.Name); + var children = rootAreas.Select(a => BuildAreaNode(a, site.Id)).ToList(); + _treeRoots.Add(new AreaTreeNode( + Key: $"site-{site.Id}", + Label: site.Name, + Kind: AreaNodeKind.Site, + Children: children, + SiteId: site.Id, + AreaCount: areas.Count)); + } } catch (Exception ex) { - _errorMessage = $"Failed to load sites: {ex.Message}"; + _errorMessage = $"Failed to load data: {ex.Message}"; } _loading = false; } - private async Task SelectSite(int siteId) + private AreaTreeNode BuildAreaNode(Area area, int siteId) { - _selectedSiteId = siteId; - _showForm = false; - await LoadAreasAsync(); - } - - private async Task LoadAreasAsync() - { - try - { - _areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList(); - } - catch (Exception ex) - { - _errorMessage = $"Failed to load areas: {ex.Message}"; - } - } - - private List _rootAreas => _areas.Where(a => a.ParentAreaId == null).OrderBy(a => a.Name).ToList(); - - private string GetAreaPath(Area area) - { - var parts = new List(); - var current = area; - while (current != null) - { - parts.Insert(0, current.Name); - current = current.ParentAreaId.HasValue - ? _areas.FirstOrDefault(a => a.Id == current.ParentAreaId.Value) - : null; - } - return string.Join(" / ", parts); - } - - private void ShowAddForm() - { - _editingArea = null; - _formName = string.Empty; - _formParentAreaId = 0; - _formError = null; - _showForm = true; - } - - private void EditArea(Area area) - { - _editingArea = area; - _formName = area.Name; - _formError = null; - _showForm = true; - } - - private void CancelForm() - { - _showForm = false; - _editingArea = null; - _formError = null; - } - - private async Task SaveArea() - { - _formError = null; - if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; } - - try - { - if (_editingArea != null) - { - _editingArea.Name = _formName.Trim(); - await TemplateEngineRepository.UpdateAreaAsync(_editingArea); - } - else - { - var area = new Area(_formName.Trim()) - { - SiteId = _selectedSiteId, - ParentAreaId = _formParentAreaId == 0 ? null : _formParentAreaId - }; - await TemplateEngineRepository.AddAreaAsync(area); - } - await TemplateEngineRepository.SaveChangesAsync(); - _showForm = false; - _editingArea = null; - _toast.ShowSuccess("Area saved."); - await LoadAreasAsync(); - } - catch (Exception ex) - { - _formError = $"Save failed: {ex.Message}"; - } - } - - private async Task DeleteArea(Area area) - { - var hasChildren = _areas.Any(a => a.ParentAreaId == area.Id); - var message = hasChildren - ? $"Area '{area.Name}' has child areas. Delete child areas first." - : $"Delete area '{area.Name}'?"; - - var confirmed = await _confirmDialog.ShowAsync(message, "Delete Area"); - if (!confirmed) return; - - try - { - await TemplateEngineRepository.DeleteAreaAsync(area.Id); - await TemplateEngineRepository.SaveChangesAsync(); - _toast.ShowSuccess($"Area '{area.Name}' deleted."); - await LoadAreasAsync(); - } - catch (Exception ex) - { - _toast.ShowError($"Delete failed: {ex.Message}"); - } + var children = area.Children + .OrderBy(c => c.Name) + .Select(c => BuildAreaNode(c, siteId)) + .ToList(); + return new AreaTreeNode( + Key: $"area-{area.Id}", + Label: area.Name, + Kind: AreaNodeKind.Area, + Children: children, + SiteId: siteId, + Area: area); } }