271 lines
9.7 KiB
Plaintext
271 lines
9.7 KiB
Plaintext
@page "/admin/areas"
|
|
@using ScadaLink.Security
|
|
@using ScadaLink.Commons.Entities.Instances
|
|
@using ScadaLink.Commons.Entities.Sites
|
|
@using ScadaLink.Commons.Interfaces.Repositories
|
|
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
|
@inject ISiteRepository SiteRepository
|
|
@inject ITemplateEngineRepository TemplateEngineRepository
|
|
|
|
<div class="container-fluid mt-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">Area Management</h4>
|
|
</div>
|
|
|
|
<ToastNotification @ref="_toast" />
|
|
<ConfirmDialog @ref="_confirmDialog" />
|
|
|
|
@if (_loading)
|
|
{
|
|
<LoadingSpinner IsLoading="true" />
|
|
}
|
|
else if (_errorMessage != null)
|
|
{
|
|
<div class="alert alert-danger">@_errorMessage</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">Sites</h6>
|
|
</div>
|
|
<div class="list-group list-group-flush">
|
|
@if (_sites.Count == 0)
|
|
{
|
|
<div class="list-group-item text-muted small">No sites configured.</div>
|
|
}
|
|
@foreach (var site in _sites)
|
|
{
|
|
<button type="button"
|
|
class="list-group-item list-group-item-action @(site.Id == _selectedSiteId ? "active" : "")"
|
|
@onclick="() => SelectSite(site.Id)">
|
|
@site.Name
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-9">
|
|
@if (_selectedSiteId == 0)
|
|
{
|
|
<div class="text-muted">Select a site to manage its areas.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5 class="mb-0">Areas for @(_sites.FirstOrDefault(s => s.Id == _selectedSiteId)?.Name)</h5>
|
|
<button class="btn btn-primary btn-sm" @onclick="ShowAddForm">Add Area</button>
|
|
</div>
|
|
|
|
@if (_showForm)
|
|
{
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h6 class="card-title">@(_editingArea == null ? "Add New Area" : "Edit Area")</h6>
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Name</label>
|
|
<input type="text" class="form-control form-control-sm" @bind="_formName" />
|
|
</div>
|
|
@if (_editingArea == null)
|
|
{
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Parent Area</label>
|
|
<select class="form-select form-select-sm" @bind="_formParentAreaId">
|
|
<option value="0">(Root level)</option>
|
|
@foreach (var area in _areas)
|
|
{
|
|
<option value="@area.Id">@GetAreaPath(area)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
}
|
|
<div class="col-md-4">
|
|
<button class="btn btn-success btn-sm me-1" @onclick="SaveArea">Save</button>
|
|
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelForm">Cancel</button>
|
|
</div>
|
|
</div>
|
|
@if (_formError != null)
|
|
{
|
|
<div class="text-danger small mt-1">@_formError</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<TreeView TItem="Area" Items="@_rootAreas"
|
|
ChildrenSelector='a => a.Children.OrderBy(c => c.Name).ToList()'
|
|
HasChildrenSelector="a => a.Children.Any()"
|
|
KeySelector="a => (object)a.Id"
|
|
StorageKey="areas-tree">
|
|
<NodeContent Context="area">
|
|
<span>@area.Name</span>
|
|
</NodeContent>
|
|
<ContextMenu Context="area">
|
|
<button class="dropdown-item" @onclick="() => EditArea(area)">
|
|
Edit
|
|
</button>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="dropdown-item text-danger" @onclick="() => DeleteArea(area)">
|
|
Delete
|
|
</button>
|
|
</ContextMenu>
|
|
<EmptyContent>
|
|
<span class="text-muted fst-italic">No areas for this site. Add one above.</span>
|
|
</EmptyContent>
|
|
</TreeView>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
private List<Site> _sites = new();
|
|
private List<Area> _areas = new();
|
|
private int _selectedSiteId;
|
|
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()
|
|
{
|
|
_loading = true;
|
|
try
|
|
{
|
|
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = $"Failed to load sites: {ex.Message}";
|
|
}
|
|
_loading = false;
|
|
}
|
|
|
|
private async Task SelectSite(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<Area> _rootAreas => _areas.Where(a => a.ParentAreaId == null).OrderBy(a => a.Name).ToList();
|
|
|
|
private string GetAreaPath(Area area)
|
|
{
|
|
var parts = new List<string>();
|
|
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}");
|
|
}
|
|
}
|
|
}
|