Files
Denso/srcs/RobotNet10/FleetManager/RobotNet10.FleetManager.Client/Components/RobotDetail/ManualOrderPanel.razor
2026-07-03 16:31:37 +07:00

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