402 lines
14 KiB
Plaintext
402 lines
14 KiB
Plaintext
@using RobotNet10.FleetManager.Shared.DTOs.Robot
|
|
@using RobotNet10.FleetManager.Client.Services
|
|
@using RobotNet10.MapEditor.Services.API
|
|
@using RobotNet10.MapEditor.Shared.DTOs.LayoutData
|
|
@using RobotNet10.MapEditor.Shared.DTOs.Node
|
|
@using RobotNet10.FleetManager.Shared.Models
|
|
@using RobotNet10.Shared
|
|
@using System.Net.Http.Json
|
|
@inject HttpClient HttpClient
|
|
@inject ISnackbar Snackbar
|
|
@inject RobotApiService RobotApiService
|
|
@inject MapManagerApiService MapApiService
|
|
|
|
<MudPaper Class="pa-4" Elevation="0">
|
|
<MudText Typo="Typo.h6" Class="mb-4">Manual Order</MudText>
|
|
|
|
<MudStack Spacing="3">
|
|
<!-- Node Selector -->
|
|
<MudSelect @bind-Value="selectedNodeName"
|
|
Label="Select Node"
|
|
Variant="Variant.Outlined"
|
|
T="string"
|
|
Disabled="@isLoadingNodes"
|
|
ErrorText="@GetValidationError("NodeName")">
|
|
@if (isLoadingNodes)
|
|
{
|
|
<MudSelectItem Value="@(string.Empty)" Disabled="true">Loading nodes...</MudSelectItem>
|
|
}
|
|
else if (nodes != null && nodes.Any())
|
|
{
|
|
<MudSelectItem Value="@(string.Empty)">-- Select Node --</MudSelectItem>
|
|
@foreach (var node in nodes.Where(n => !string.IsNullOrEmpty(n.NodeName)).OrderBy(n => n.NodeName))
|
|
{
|
|
var displayName = !string.IsNullOrWhiteSpace(node.NodeName)
|
|
? $"{node.NodeName} ({node.NodeId})"
|
|
: node.NodeId;
|
|
<MudSelectItem Value="@node.NodeName">@displayName</MudSelectItem>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<MudSelectItem Value="@(string.Empty)" Disabled="true">No nodes available</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(selectedNodeName))
|
|
{
|
|
var selectedNode = nodes?.FirstOrDefault(n =>
|
|
(!string.IsNullOrWhiteSpace(n.NodeName) && n.NodeName == selectedNodeName) ||
|
|
(string.IsNullOrWhiteSpace(n.NodeName) && n.NodeId == selectedNodeName));
|
|
@if (selectedNode != null)
|
|
{
|
|
<MudPaper Class="pa-2" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
|
<MudText Typo="Typo.body2">
|
|
<strong>Node ID:</strong> @selectedNode.NodeId<br />
|
|
@if (!string.IsNullOrWhiteSpace(selectedNode.NodeDescription))
|
|
{
|
|
<strong>Description:</strong> @selectedNode.NodeDescription<br />
|
|
}
|
|
<strong>Position:</strong> X: @selectedNode.X.ToString("F2")m, Y: @selectedNode.Y.ToString("F2")m
|
|
</MudText>
|
|
</MudPaper>
|
|
}
|
|
}
|
|
|
|
<!-- Action Buttons -->
|
|
<MudStack Row="true" Spacing="2">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Send"
|
|
OnClick="SendOrder"
|
|
Disabled="@(isSending || string.IsNullOrWhiteSpace(selectedNodeName))"
|
|
FullWidth="true">
|
|
@if (isSending)
|
|
{
|
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
<span>Sending...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Send Order</span>
|
|
}
|
|
</MudButton>
|
|
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Error"
|
|
StartIcon="@Icons.Material.Filled.Cancel"
|
|
OnClick="CancelOrder"
|
|
Disabled="@isCanceling"
|
|
FullWidth="true">
|
|
@if (isCanceling)
|
|
{
|
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
<span>Canceling...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Cancel Order</span>
|
|
}
|
|
</MudButton>
|
|
</MudStack>
|
|
|
|
<!-- Order History -->
|
|
<MudDivider />
|
|
<MudText Typo="Typo.subtitle2">Recent Orders</MudText>
|
|
@if (orderHistory.Count > 0)
|
|
{
|
|
<MudStack Spacing="1" Style="height: 200px; overflow: auto;">
|
|
@foreach (var order in orderHistory.Take(10))
|
|
{
|
|
<MudPaper Class="pa-2" Elevation="0">
|
|
<MudText Typo="Typo.body2">
|
|
<strong>@order.NodeName</strong> - @order.Timestamp.ToString("g")
|
|
@if (!order.IsSuccess)
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Color="Color.Error" Class="ml-2">Failed</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="ml-2">Success</MudChip>
|
|
}
|
|
</MudText>
|
|
</MudPaper>
|
|
}
|
|
</MudStack>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Align="Align.Center">No orders sent yet</MudText>
|
|
}
|
|
</MudStack>
|
|
</MudPaper>
|
|
|
|
@code {
|
|
[Parameter]
|
|
public string RobotId { get; set; } = string.Empty;
|
|
|
|
[Parameter]
|
|
public Guid? MapId { get; set; }
|
|
|
|
private string selectedNodeName = string.Empty;
|
|
private List<NodeDto> nodes = new();
|
|
private List<SentOrder> orderHistory = new();
|
|
private bool isSending = false;
|
|
private bool isCanceling = false;
|
|
private bool isLoadingNodes = false;
|
|
private Dictionary<string, string> validationErrors = new();
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadNodesAsync();
|
|
}
|
|
|
|
private async Task LoadNodesAsync()
|
|
{
|
|
if (!MapId.HasValue || MapId.Value == Guid.Empty)
|
|
{
|
|
// Try to get MapId from robot
|
|
try
|
|
{
|
|
var robot = await RobotApiService.GetByRobotIdAsync(RobotId);
|
|
if (robot?.MapId.HasValue == true)
|
|
{
|
|
MapId = robot.MapId;
|
|
}
|
|
else
|
|
{
|
|
nodes = new List<NodeDto>();
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error loading robot: {ex.Message}", Severity.Warning);
|
|
nodes = new List<NodeDto>();
|
|
return;
|
|
}
|
|
}
|
|
|
|
isLoadingNodes = true;
|
|
StateHasChanged();
|
|
try
|
|
{
|
|
var layoutData = await MapApiService.GetLayoutDataAsync(MapId.Value);
|
|
nodes = layoutData.Nodes?.ToList() ?? new List<NodeDto>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error loading nodes: {ex.Message}", Severity.Warning);
|
|
nodes = new List<NodeDto>();
|
|
}
|
|
finally
|
|
{
|
|
isLoadingNodes = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private string? GetValidationError(string fieldName)
|
|
{
|
|
return validationErrors.TryGetValue(fieldName, out var error) ? error : null;
|
|
}
|
|
|
|
private async Task SendOrder()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(RobotId))
|
|
{
|
|
Snackbar.Add("Robot ID is required", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(selectedNodeName))
|
|
{
|
|
validationErrors["NodeName"] = "Please select a node";
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
isSending = true;
|
|
validationErrors.Clear();
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
// Find the selected node to get NodeName (preferred) or NodeId
|
|
var selectedNode = nodes?.FirstOrDefault(n =>
|
|
(!string.IsNullOrWhiteSpace(n.NodeName) && n.NodeName == selectedNodeName) ||
|
|
(string.IsNullOrWhiteSpace(n.NodeName) && n.NodeId == selectedNodeName));
|
|
|
|
if (selectedNode == null)
|
|
{
|
|
Snackbar.Add("Selected node not found", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
// Use NodeName if available, otherwise use NodeId
|
|
var nodeNameToSend = !string.IsNullOrWhiteSpace(selectedNode.NodeName)
|
|
? selectedNode.NodeName
|
|
: selectedNode.NodeId;
|
|
|
|
// Build order request
|
|
var orderRequest = new RobotMoveToNodeModel
|
|
{
|
|
RobotId = RobotId,
|
|
NodeName = nodeNameToSend,
|
|
LastAngle = null, // Optional, can be enhanced later
|
|
};
|
|
|
|
// Call API endpoint
|
|
var response = await HttpClient.PostAsJsonAsync($"/api/RobotManager/MoveToNode", orderRequest);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
|
if (result is null)
|
|
{
|
|
Snackbar.Add($"Sending order failed", Severity.Warning);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
else if (!result.IsSuccess)
|
|
{
|
|
Snackbar.Add($"{result.Message}", Severity.Warning);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add($"Order to node '{selectedNodeName}' sent successfully", Severity.Success);
|
|
|
|
// Add to history
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = true
|
|
});
|
|
|
|
// Clear selection
|
|
selectedNodeName = string.Empty;
|
|
}
|
|
}
|
|
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
{
|
|
Snackbar.Add($"Robot with ID '{RobotId}' not found", Severity.Error);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
|
{
|
|
var error = await response.Content.ReadAsStringAsync();
|
|
Snackbar.Add($"Invalid order request: {error}", Severity.Warning);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var error = await response.Content.ReadAsStringAsync();
|
|
Snackbar.Add($"Error sending order: {error}", Severity.Error);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error sending order: {ex.Message}", Severity.Error);
|
|
orderHistory.Insert(0, new SentOrder
|
|
{
|
|
NodeName = selectedNodeName ?? "Unknown",
|
|
Timestamp = DateTime.UtcNow,
|
|
IsSuccess = false
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
isSending = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task CancelOrder()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(RobotId))
|
|
{
|
|
Snackbar.Add("Robot ID is required", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
isCanceling = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
// Call API endpoint
|
|
var response = await HttpClient.DeleteAsync($"/api/RobotManager/MoveToNode/{RobotId}");
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
|
if (result is null)
|
|
{
|
|
Snackbar.Add($"Canceling order failed", Severity.Warning);
|
|
}
|
|
else if (!result.IsSuccess)
|
|
{
|
|
Snackbar.Add($"{result.Message}", Severity.Warning);
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add($"Order canceled successfully", Severity.Success);
|
|
}
|
|
}
|
|
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
{
|
|
Snackbar.Add($"Robot with ID '{RobotId}' not found", Severity.Error);
|
|
}
|
|
else
|
|
{
|
|
var error = await response.Content.ReadAsStringAsync();
|
|
Snackbar.Add($"Error canceling order: {error}", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error canceling order: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
isCanceling = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
// Helper class for order history
|
|
private class SentOrder
|
|
{
|
|
public string NodeName { get; set; } = string.Empty;
|
|
public DateTime Timestamp { get; set; }
|
|
public bool IsSuccess { get; set; }
|
|
}
|
|
}
|