Initial commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
@using RobotNet10.MapEditor.Services.State
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Requests
|
||||
@inject StationManagerState State
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Add" Class="mr-2" />
|
||||
Create New Station
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<!-- Station ID -->
|
||||
<MudTextField @bind-Value="request.StationId"
|
||||
Label="Station ID *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="Unique identifier (must be unique within this level)"
|
||||
Validation="@(new Func<string, string?>(ValidateStationId))" />
|
||||
|
||||
<!-- Station Name -->
|
||||
<MudTextField @bind-Value="request.StationName"
|
||||
Label="Station Name"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="Display name for this station" />
|
||||
|
||||
<!-- Description -->
|
||||
<MudTextField @bind-Value="request.StationDescription"
|
||||
Label="Description"
|
||||
Lines="3"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="Optional description" />
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
<!-- Position Section -->
|
||||
<MudText Typo="Typo.subtitle2">Position</MudText>
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField @bind-Value="request.X"
|
||||
Label="X (meters) *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
Format="F3"
|
||||
Step="0.1" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField @bind-Value="request.Y"
|
||||
Label="Y (meters) *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
Format="F3"
|
||||
Step="0.1" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField @bind-Value="request.Theta"
|
||||
Label="Theta (radians)"
|
||||
Variant="Variant.Outlined"
|
||||
Format="F3"
|
||||
Step="0.1"
|
||||
HelperText="Optional, range: -π to π" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField @bind-Value="request.StationHeight"
|
||||
Label="Height (meters)"
|
||||
Variant="Variant.Outlined"
|
||||
Format="F2"
|
||||
Step="0.1"
|
||||
HelperText="Optional" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- Validation Messages -->
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@errorMessage</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!IsValid() || isSubmitting)">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Creating...</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Create</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Guid LayoutLevelId { get; set; }
|
||||
|
||||
private CreateStationRequest request = new();
|
||||
private bool isSubmitting = false;
|
||||
private string? errorMessage;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
request.LayoutLevelId = LayoutLevelId;
|
||||
|
||||
// Set default position
|
||||
request.X = 0;
|
||||
request.Y = 0;
|
||||
}
|
||||
|
||||
private bool IsValid()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(request.StationId);
|
||||
}
|
||||
|
||||
private string? ValidateStationId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return "Station ID is required";
|
||||
|
||||
if (value.Length > 128)
|
||||
return "Station ID must be 128 characters or less";
|
||||
|
||||
// Check if already exists in current stations
|
||||
if (State.Stations.Any(s => s.StationId.Equals(value, StringComparison.OrdinalIgnoreCase)))
|
||||
return "Station ID already exists in this level";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
errorMessage = null;
|
||||
|
||||
// Validate theta range
|
||||
if (request.Theta.HasValue && (request.Theta.Value < -Math.PI || request.Theta.Value > Math.PI))
|
||||
{
|
||||
errorMessage = "Theta must be between -π and π";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate station height
|
||||
if (request.StationHeight.HasValue && request.StationHeight.Value < 0)
|
||||
{
|
||||
errorMessage = "Station height must be >= 0";
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting = true;
|
||||
|
||||
try
|
||||
{
|
||||
var created = await State.CreateStationAsync(request);
|
||||
MudDialog.Close(DialogResult.Ok(created));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
@using RobotNet10.MapEditor.Services.State
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Station
|
||||
@inject StationManagerState State
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-2" />
|
||||
Delete Station
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudText>
|
||||
Are you sure you want to delete this station?
|
||||
</MudText>
|
||||
|
||||
<!-- Station Info -->
|
||||
<MudPaper Class="pa-3" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">Station ID:</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@Station.StationId</strong></MudText>
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrEmpty(Station.StationName))
|
||||
{
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">Name:</MudText>
|
||||
<MudText Typo="Typo.body2">@Station.StationName</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">Position:</MudText>
|
||||
<MudText Typo="Typo.body2">(@Station.X.ToString("F2"), @Station.Y.ToString("F2"))</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">Interaction Nodes:</MudText>
|
||||
<MudText Typo="Typo.body2">@(Station.InteractionNodes?.Count ?? 0)</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- Warning about interaction nodes -->
|
||||
@if (Station.InteractionNodes != null && Station.InteractionNodes.Count > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
This station has @Station.InteractionNodes.Count interaction node(s).
|
||||
The links to these nodes will be removed, but the nodes themselves will not be deleted.
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudAlert Severity="Severity.Error">
|
||||
<strong>This action cannot be undone.</strong>
|
||||
</MudAlert>
|
||||
|
||||
<!-- Error Message -->
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">
|
||||
@errorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Disabled="@isDeleting">Cancel</MudButton>
|
||||
<MudButton Color="Color.Error"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Delete"
|
||||
Disabled="@isDeleting">
|
||||
@if (isDeleting)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Deleting...</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Delete Station</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public StationDto Station { get; set; } = null!;
|
||||
|
||||
private bool isDeleting = false;
|
||||
private string? errorMessage;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
errorMessage = null;
|
||||
isDeleting = true;
|
||||
|
||||
try
|
||||
{
|
||||
await State.DeleteStationAsync(Station.Id);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
isDeleting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
@using RobotNet10.MapEditor.Services.State
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Station
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Node
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Requests
|
||||
@using RobotNet10.MapEditor.Components.StationManager.Dialogs
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject MapManagerApiService ApiService
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4" Style="height: calc(100vh - 180px); overflow-y: auto;">
|
||||
@if (State.SelectedStation == null)
|
||||
{
|
||||
<!-- No Selection State -->
|
||||
<MudStack AlignItems="AlignItems.Center" Justify="Justify.Center" Style="min-height: 500px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TouchApp" Size="Size.Large" Color="Color.Secondary" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Secondary">No Station Selected</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
Select a station from the list to view and edit details.
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Station Details -->
|
||||
<MudStack Spacing="3">
|
||||
<!-- Header -->
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.h6">Station Details</MudText>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@if (isEditing)
|
||||
{
|
||||
<MudTooltip Text="Cancel">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
||||
Size="Size.Small"
|
||||
OnClick="CancelEdit" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Save Changes">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success"
|
||||
OnClick="SaveChanges"
|
||||
Disabled="@State.IsSaving" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="Edit">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="StartEdit" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Delete">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="HandleDelete" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
<MudDivider />
|
||||
|
||||
@if (State.IsSaving)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
|
||||
<!-- Basic Info -->
|
||||
<MudPaper Class="pa-3" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">STATION ID (READ-ONLY)</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@State.SelectedStation.StationId</strong></MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- Editable Fields -->
|
||||
<MudTextField Label="Station Name"
|
||||
@bind-Value="editModel.StationName"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@(!isEditing)"
|
||||
HelperText="Display name for this station" />
|
||||
|
||||
<MudTextField Label="Description"
|
||||
@bind-Value="editModel.StationDescription"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
ReadOnly="@(!isEditing)"
|
||||
HelperText="Optional description" />
|
||||
|
||||
<!-- Position Section -->
|
||||
<MudText Typo="Typo.subtitle2" Class="mt-2">Position</MudText>
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField Label="X (meters)"
|
||||
@bind-Value="editModel.X"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@(!isEditing)"
|
||||
Format="F3"
|
||||
Step="0.1" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField Label="Y (meters)"
|
||||
@bind-Value="editModel.Y"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@(!isEditing)"
|
||||
Format="F3"
|
||||
Step="0.1" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField Label="Theta (radians)"
|
||||
@bind-Value="editModel.Theta"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@(!isEditing)"
|
||||
Format="F3"
|
||||
Step="0.1"
|
||||
HelperText="Range: -π to π" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField Label="Height (meters)"
|
||||
@bind-Value="editModel.StationHeight"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@(!isEditing)"
|
||||
Format="F2"
|
||||
Step="0.1"
|
||||
HelperText="Optional" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- Interaction Nodes Section -->
|
||||
<MudText Typo="Typo.subtitle2" Class="mt-2">
|
||||
Interaction Nodes (@(editModel.InteractionNodeIds?.Count ?? 0))
|
||||
</MudText>
|
||||
|
||||
@if (isEditing)
|
||||
{
|
||||
<MudPaper Class="pa-2" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
||||
<MudStack Spacing="2">
|
||||
@if (availableNodes == null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Size="Size.Small" />
|
||||
}
|
||||
else if (editModel.InteractionNodeIds != null && editModel.InteractionNodeIds.Count > 0)
|
||||
{
|
||||
<MudStack Spacing="1">
|
||||
@foreach (var nodeId in editModel.InteractionNodeIds.ToList())
|
||||
{
|
||||
var node = availableNodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
<MudChip T="string"
|
||||
OnClose="() => RemoveInteractionNode(nodeId)"
|
||||
Size="Size.Small"
|
||||
Color="Color.Info">
|
||||
@if (node != null)
|
||||
{
|
||||
<text>@node.NodeId (@node.X.ToString("F2"), @node.Y.ToString("F2"))</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Node @nodeId</text>
|
||||
}
|
||||
</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">No interaction nodes</MudText>
|
||||
}
|
||||
|
||||
@if (availableNodes != null && availableNodes.Count > 0)
|
||||
{
|
||||
<MudAutocomplete T="NodeDto"
|
||||
Label="Add Interaction Node"
|
||||
SearchFunc="SearchNodes"
|
||||
ToStringFunc="@(n => n == null ? "" : $"{n.NodeId} ({n.X:F2}, {n.Y:F2})")"
|
||||
ValueChanged="AddInteractionNode"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
Clearable="true"
|
||||
ResetValueOnEmptyText="true" />
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-2" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
||||
@if (State.SelectedStation.InteractionNodes != null && State.SelectedStation.InteractionNodes.Count > 0)
|
||||
{
|
||||
<MudStack Spacing="1">
|
||||
@foreach (var interactionNode in State.SelectedStation.InteractionNodes)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">
|
||||
@if (interactionNode.Node != null)
|
||||
{
|
||||
<text>@interactionNode.Node.NodeId (@interactionNode.Node.X.ToString("F2"), @interactionNode.Node.Y.ToString("F2"))</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Node @interactionNode.NodeId</text>
|
||||
}
|
||||
</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">No interaction nodes</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<!-- Metadata -->
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public StationManagerState State { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnStationDeleted { get; set; }
|
||||
|
||||
private bool isEditing = false;
|
||||
private EditModel editModel = new();
|
||||
private List<NodeDto>? availableNodes;
|
||||
|
||||
private class EditModel
|
||||
{
|
||||
public string? StationName { get; set; }
|
||||
public string? StationDescription { get; set; }
|
||||
public double? X { get; set; }
|
||||
public double? Y { get; set; }
|
||||
public double? Theta { get; set; }
|
||||
public double? StationHeight { get; set; }
|
||||
public List<Guid>? InteractionNodeIds { get; set; }
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (State.SelectedStation != null && !isEditing)
|
||||
{
|
||||
LoadEditModel();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadEditModel()
|
||||
{
|
||||
if (State.SelectedStation == null) return;
|
||||
|
||||
editModel = new EditModel
|
||||
{
|
||||
StationName = State.SelectedStation.StationName,
|
||||
StationDescription = State.SelectedStation.StationDescription,
|
||||
X = State.SelectedStation.X,
|
||||
Y = State.SelectedStation.Y,
|
||||
Theta = State.SelectedStation.Theta,
|
||||
StationHeight = State.SelectedStation.StationHeight,
|
||||
InteractionNodeIds = State.SelectedStation.InteractionNodes?
|
||||
.Select(i => i.NodeId)
|
||||
.ToList() ?? new List<Guid>()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task StartEdit()
|
||||
{
|
||||
isEditing = true;
|
||||
LoadEditModel();
|
||||
|
||||
// Load available nodes for autocomplete
|
||||
if (State.CurrentLayoutLevelId.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var layoutData = await ApiService.GetLayoutDataAsync(State.CurrentLayoutLevelId.Value);
|
||||
availableNodes = layoutData.Nodes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to load nodes: {ex.Message}", Severity.Warning);
|
||||
availableNodes = new List<NodeDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
isEditing = false;
|
||||
LoadEditModel();
|
||||
availableNodes = null;
|
||||
}
|
||||
|
||||
private async Task SaveChanges()
|
||||
{
|
||||
if (State.SelectedStation == null) return;
|
||||
|
||||
// Validate theta range
|
||||
if (editModel.Theta.HasValue && (editModel.Theta.Value < -Math.PI || editModel.Theta.Value > Math.PI))
|
||||
{
|
||||
Snackbar.Add("Theta must be between -π and π", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate station height
|
||||
if (editModel.StationHeight.HasValue && editModel.StationHeight.Value < 0)
|
||||
{
|
||||
Snackbar.Add("Station height must be >= 0", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new UpdateStationRequest
|
||||
{
|
||||
StationName = editModel.StationName,
|
||||
StationDescription = editModel.StationDescription,
|
||||
X = editModel.X,
|
||||
Y = editModel.Y,
|
||||
Theta = editModel.Theta,
|
||||
StationHeight = editModel.StationHeight,
|
||||
InteractionNodeIds = editModel.InteractionNodeIds
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await State.UpdateStationAsync(State.SelectedStation.Id, request);
|
||||
isEditing = false;
|
||||
availableNodes = null;
|
||||
Snackbar.Add("Station updated successfully", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to update station: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDelete()
|
||||
{
|
||||
if (State.SelectedStation == null) return;
|
||||
|
||||
var parameters = new DialogParameters<DeleteStationDialog>
|
||||
{
|
||||
{ x => x.Station, State.SelectedStation }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<DeleteStationDialog>(
|
||||
"Delete Station",
|
||||
parameters,
|
||||
new DialogOptions { MaxWidth = MaxWidth.Small });
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result != null && !result.Canceled)
|
||||
{
|
||||
await OnStationDeleted.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private Task<IEnumerable<NodeDto>> SearchNodes(string value, CancellationToken token)
|
||||
{
|
||||
if (availableNodes == null)
|
||||
return Task.FromResult<IEnumerable<NodeDto>>(Array.Empty<NodeDto>());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return Task.FromResult<IEnumerable<NodeDto>>(availableNodes);
|
||||
|
||||
var searchLower = value.ToLowerInvariant();
|
||||
var filtered = availableNodes
|
||||
.Where(n => n.NodeId.ToLower().Contains(searchLower) ||
|
||||
(n.NodeName != null && n.NodeName.ToLower().Contains(searchLower)))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IEnumerable<NodeDto>>(filtered);
|
||||
}
|
||||
|
||||
private void AddInteractionNode(NodeDto? node)
|
||||
{
|
||||
if (node == null || editModel.InteractionNodeIds == null) return;
|
||||
|
||||
if (!editModel.InteractionNodeIds.Contains(node.Id))
|
||||
{
|
||||
editModel.InteractionNodeIds.Add(node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveInteractionNode(Guid nodeId)
|
||||
{
|
||||
editModel.InteractionNodeIds?.Remove(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
@using RobotNet10.MapEditor.Services.State
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Station
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4" Style="height: calc(100vh - 180px); overflow-y: hidden;">
|
||||
<MudStack Spacing="3">
|
||||
<!-- Header -->
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.h6">
|
||||
Stations
|
||||
</MudText>
|
||||
@if (State.IsLoading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudDivider />
|
||||
|
||||
<!-- Stations Table -->
|
||||
@if (State.IsLoading && State.Stations.Count == 0)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Justify="Justify.Center" Style="min-height: 400px;">
|
||||
<MudProgressCircular Size="Size.Large" Indeterminate="true" />
|
||||
<MudText>Loading stations...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(State.ErrorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">
|
||||
@State.ErrorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
else if (State.GetFilteredStations().Count == 0)
|
||||
{
|
||||
<!-- Empty State -->
|
||||
<MudStack AlignItems="AlignItems.Center" Justify="Justify.Center" Style="min-height: 400px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Store" Size="Size.Large" Color="Color.Secondary" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Secondary">No Stations Found</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
@if (!string.IsNullOrWhiteSpace(State.SearchQuery))
|
||||
{
|
||||
<text>No stations match your search criteria.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Click "Add Station" to create your first station.</text>
|
||||
}
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="@State.GetFilteredStations()"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
FixedHeader="true"
|
||||
Height="calc(100vh - 342px)"
|
||||
SelectedItem="@State.SelectedStation"
|
||||
Elevation="0"
|
||||
SelectedItemChanged="OnStationSelected"
|
||||
T="StationDto">
|
||||
<HeaderContent>
|
||||
<MudTh>Station ID</MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh Style="text-align: right;">Position (X, Y)</MudTh>
|
||||
<MudTh Style="text-align: right;">Theta</MudTh>
|
||||
<MudTh Style="text-align: center;">Nodes</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Station ID">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Store" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2"><strong>@context.StationId</strong></MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Name">
|
||||
<MudText Typo="Typo.body2">
|
||||
@(context.StationName ?? "-")
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Position" Style="text-align: right;">
|
||||
<MudText Typo="Typo.body2" Style="font-family: monospace;">
|
||||
(@context.X.ToString("F2"), @context.Y.ToString("F2"))
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Theta" Style="text-align: right;">
|
||||
<MudText Typo="Typo.body2" Style="font-family: monospace;">
|
||||
@(context.Theta.HasValue ? context.Theta.Value.ToString("F3") : "-")
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Nodes" Style="text-align: center;">
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">
|
||||
@(context.InteractionNodes?.Count ?? 0)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<div class="d-flex w-100 flex-row-reverse">
|
||||
<MudTablePager HideRowsPerPage Style="width: 100%;" PageSizeOptions="new[] { 25, 100, 200 }" />
|
||||
</div>
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public StationManagerState State { get; set; } = null!;
|
||||
|
||||
private async Task OnStationSelected(StationDto? station)
|
||||
{
|
||||
if (station != null)
|
||||
{
|
||||
await State.SelectStationAsync(station.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
@using RobotNet10.MapEditor.Services.State
|
||||
@using RobotNet10.MapEditor.Shared.DTOs.Station
|
||||
@using RobotNet10.MapEditor.Components.StationManager
|
||||
@using RobotNet10.MapEditor.Components.StationManager.Dialogs
|
||||
@inject StationManagerState State
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@implements IDisposable
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<!-- Header -->
|
||||
<MudPaper Class="pa-4 mb-4" MinHeight="80px">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Store" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h5">Station Management</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTextField @bind-Value="searchText"
|
||||
Placeholder="Search stations..."
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Margin="Margin.Dense"
|
||||
Style="min-width: 250px;"
|
||||
Immediate="true"
|
||||
DebounceInterval="300"
|
||||
Clearable="true" />
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="HandleCreate"
|
||||
Disabled="@(!State.CurrentLayoutLevelId.HasValue)">
|
||||
Add Station
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@if (!State.CurrentLayoutLevelId.HasValue)
|
||||
{
|
||||
<!-- No Level Selected -->
|
||||
<MudPaper Class="pa-8 text-center" Elevation="0">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Info" Size="Size.Large" Color="Color.Info" Class="mb-4" />
|
||||
<MudText Typo="Typo.h6" Class="mb-2">No Layout Level Selected</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary">
|
||||
Please open this component from the Layout Editor or select a layout level.
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Main Content -->
|
||||
<MudGrid>
|
||||
<!-- Left: List Panel -->
|
||||
<MudItem xs="12" md="7">
|
||||
<StationListPanel @ref="StationListPanelRef" State="@State" />
|
||||
</MudItem>
|
||||
|
||||
<!-- Right: Details Panel -->
|
||||
<MudItem xs="12" md="5">
|
||||
<StationDetailsPanel State="@State" OnStationDeleted="HandleStationDeleted" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Guid? LayoutLevelId { get; set; }
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
private string searchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
if (_searchText != value)
|
||||
{
|
||||
_searchText = value;
|
||||
State.Search(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private StationListPanel? StationListPanelRef;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
State.OnStateChanged += StateHasChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (LayoutLevelId.HasValue && LayoutLevelId.Value != State.CurrentLayoutLevelId)
|
||||
{
|
||||
await State.InitializeAsync(LayoutLevelId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
State.OnStateChanged -= StateHasChanged;
|
||||
}
|
||||
|
||||
private async Task HandleCreate()
|
||||
{
|
||||
if (!State.CurrentLayoutLevelId.HasValue)
|
||||
{
|
||||
Snackbar.Add("No layout level selected", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<CreateStationDialog>
|
||||
{
|
||||
{ x => x.LayoutLevelId, State.CurrentLayoutLevelId.Value }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateStationDialog>(
|
||||
"Create Station",
|
||||
parameters,
|
||||
new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result != null && !result.Canceled)
|
||||
{
|
||||
await State.LoadStationsAsync();
|
||||
Snackbar.Add("Station created successfully", Severity.Success);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStationDeleted()
|
||||
{
|
||||
await State.LoadStationsAsync();
|
||||
Snackbar.Add("Station deleted successfully", Severity.Success);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user