Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}