Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,693 @@
|
||||
# Blazor UI Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the architecture and implementation approach for the Blazor WebAssembly user interface, including the component structure, state management, SignalR integration, and service layer design.
|
||||
|
||||
## Architecture
|
||||
|
||||
### High-Level Component Diagram
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| App.razor |
|
||||
| - Router |
|
||||
| - CascadingAuthenticationState |
|
||||
+------------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------------------+
|
||||
| MainLayout.razor |
|
||||
| - RadzenLayout (Header, Body, Footer) |
|
||||
| - AuthorizeView for user display |
|
||||
| - Navigation links |
|
||||
+------------------------------------------------------------------+
|
||||
|
|
||||
+----------------------+----------------------+
|
||||
| | |
|
||||
v v v
|
||||
+----------------+ +------------------+ +------------------+
|
||||
| Login.razor | | Searches.razor | | RefreshStatus |
|
||||
| (anonymous) | | SearchEdit.razor | | .razor |
|
||||
| | | SearchQueue.razor| | |
|
||||
+----------------+ +------------------+ +------------------+
|
||||
|
|
||||
+----------+-----------+
|
||||
| |
|
||||
v v
|
||||
+------------------+ +------------------+
|
||||
| Filter Panels | | Services |
|
||||
| (components) | | (DI injected) |
|
||||
+------------------+ +------------------+
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
NEW/src/JdeScoping.Client/
|
||||
├── wwwroot/
|
||||
│ ├── index.html
|
||||
│ ├── css/
|
||||
│ │ └── app.css
|
||||
│ └── js/
|
||||
│ └── interop.js # File download helper
|
||||
├── Layout/
|
||||
│ └── MainLayout.razor
|
||||
├── Pages/
|
||||
│ ├── Login.razor
|
||||
│ ├── NotAuthorized.razor
|
||||
│ ├── Searches.razor # Home/Search list
|
||||
│ ├── SearchEdit.razor # Create/Edit search
|
||||
│ ├── SearchQueue.razor
|
||||
│ └── RefreshStatus.razor
|
||||
├── Components/
|
||||
│ ├── FilterPanels/
|
||||
│ │ ├── TimeSpanFilterPanel.razor
|
||||
│ │ ├── WorkOrderFilterPanel.razor
|
||||
│ │ ├── ItemNumberFilterPanel.razor
|
||||
│ │ ├── ProfitCenterFilterPanel.razor
|
||||
│ │ ├── WorkCenterFilterPanel.razor
|
||||
│ │ ├── ComponentLotFilterPanel.razor
|
||||
│ │ ├── OperatorFilterPanel.razor
|
||||
│ │ └── PartOperationFilterPanel.razor
|
||||
│ └── Shared/
|
||||
│ └── LoadingIndicator.razor
|
||||
├── Services/
|
||||
│ ├── IAuthService.cs
|
||||
│ ├── AuthService.cs
|
||||
│ ├── ISearchService.cs
|
||||
│ ├── SearchService.cs
|
||||
│ ├── ILookupService.cs
|
||||
│ ├── LookupService.cs
|
||||
│ ├── IFileService.cs
|
||||
│ ├── FileService.cs
|
||||
│ ├── IRefreshStatusService.cs
|
||||
│ ├── RefreshStatusService.cs
|
||||
│ ├── IHubConnectionService.cs
|
||||
│ └── HubConnectionService.cs
|
||||
├── Auth/
|
||||
│ ├── AuthStateProvider.cs
|
||||
│ └── TokenStorageService.cs
|
||||
├── Models/
|
||||
│ ├── ValidCombination.cs
|
||||
│ ├── LoginModel.cs
|
||||
│ ├── SearchViewModel.cs
|
||||
│ ├── SearchCriteriaViewModel.cs
|
||||
│ ├── ItemViewModel.cs
|
||||
│ ├── ProfitCenterViewModel.cs
|
||||
│ ├── WorkCenterViewModel.cs
|
||||
│ ├── OperatorViewModel.cs
|
||||
│ ├── WorkOrderViewModel.cs
|
||||
│ ├── ComponentLotViewModel.cs
|
||||
│ ├── PartOperationViewModel.cs
|
||||
│ ├── DataUpdateViewModel.cs
|
||||
│ ├── SearchUpdate.cs
|
||||
│ └── StatusUpdate.cs
|
||||
├── _Imports.razor
|
||||
├── App.razor
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## Blazor WebAssembly Configuration
|
||||
|
||||
### Program.cs Service Registration
|
||||
|
||||
```csharp
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
// Configure HttpClient for API calls
|
||||
builder.Services.AddScoped(sp => new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||
});
|
||||
|
||||
// Radzen services
|
||||
builder.Services.AddRadzenComponents();
|
||||
builder.Services.AddScoped<DialogService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
||||
builder.Services.AddScoped<AuthStateProvider>();
|
||||
builder.Services.AddScoped<ITokenStorageService, TokenStorageService>();
|
||||
|
||||
// Application services
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<ISearchService, SearchService>();
|
||||
builder.Services.AddScoped<ILookupService, LookupService>();
|
||||
builder.Services.AddScoped<IFileService, FileService>();
|
||||
builder.Services.AddScoped<IRefreshStatusService, RefreshStatusService>();
|
||||
builder.Services.AddScoped<IHubConnectionService, HubConnectionService>();
|
||||
|
||||
// Logging
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
```
|
||||
|
||||
### App.razor Router Configuration
|
||||
|
||||
```razor
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### AuthStateProvider
|
||||
|
||||
Custom authentication state provider for JWT token-based auth:
|
||||
|
||||
```csharp
|
||||
public class AuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly ITokenStorageService _tokenStorage;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public AuthStateProvider(ITokenStorageService tokenStorage, HttpClient httpClient)
|
||||
{
|
||||
_tokenStorage = tokenStorage;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
var token = await _tokenStorage.GetTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var claims = ParseClaimsFromJwt(token);
|
||||
var identity = new ClaimsIdentity(claims, "jwt");
|
||||
return new AuthenticationState(new ClaimsPrincipal(identity));
|
||||
}
|
||||
|
||||
public async Task NotifyAuthenticationStateChangedAsync()
|
||||
{
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await _tokenStorage.RemoveTokenAsync();
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(
|
||||
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))));
|
||||
}
|
||||
|
||||
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
|
||||
{
|
||||
var payload = jwt.Split('.')[1];
|
||||
var jsonBytes = ParseBase64WithoutPadding(payload);
|
||||
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
||||
|
||||
return keyValuePairs!.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()!));
|
||||
}
|
||||
|
||||
private byte[] ParseBase64WithoutPadding(string base64)
|
||||
{
|
||||
switch (base64.Length % 4)
|
||||
{
|
||||
case 2: base64 += "=="; break;
|
||||
case 3: base64 += "="; break;
|
||||
}
|
||||
return Convert.FromBase64String(base64);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token Storage Service
|
||||
|
||||
Uses browser localStorage via JS interop:
|
||||
|
||||
```csharp
|
||||
public interface ITokenStorageService
|
||||
{
|
||||
Task<string?> GetTokenAsync();
|
||||
Task SetTokenAsync(string token);
|
||||
Task RemoveTokenAsync();
|
||||
}
|
||||
|
||||
public class TokenStorageService : ITokenStorageService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private const string TokenKey = "authToken";
|
||||
|
||||
public TokenStorageService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<string?> GetTokenAsync()
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", TokenKey);
|
||||
}
|
||||
|
||||
public async Task SetTokenAsync(string token)
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", TokenKey, token);
|
||||
}
|
||||
|
||||
public async Task RemoveTokenAsync()
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", TokenKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SignalR Client Integration
|
||||
|
||||
### IHubConnectionService
|
||||
|
||||
```csharp
|
||||
public interface IHubConnectionService
|
||||
{
|
||||
HubConnectionState State { get; }
|
||||
event Action<SearchUpdate>? OnSearchUpdate;
|
||||
event Action<StatusUpdate>? OnStatusUpdate;
|
||||
|
||||
Task StartAsync(CancellationToken ct = default);
|
||||
Task StopAsync(CancellationToken ct = default);
|
||||
Task<StatusUpdate?> GetCachedStatusAsync(CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
### HubConnectionService Implementation
|
||||
|
||||
```csharp
|
||||
public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
||||
{
|
||||
private readonly HubConnection _hubConnection;
|
||||
private readonly ILogger<HubConnectionService> _logger;
|
||||
|
||||
public HubConnectionState State => _hubConnection.State;
|
||||
public event Action<SearchUpdate>? OnSearchUpdate;
|
||||
public event Action<StatusUpdate>? OnStatusUpdate;
|
||||
|
||||
public HubConnectionService(
|
||||
NavigationManager navigation,
|
||||
ILogger<HubConnectionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(navigation.ToAbsoluteUri("/hubs/status"))
|
||||
.WithAutomaticReconnect(new[]
|
||||
{
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30)
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.SetMinimumLevel(LogLevel.Information);
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Register message handlers
|
||||
_hubConnection.On<SearchUpdate>("searchUpdate", update =>
|
||||
{
|
||||
_logger.LogDebug("Received searchUpdate: {SearchId} - {Status}",
|
||||
update.ID, update.Status);
|
||||
OnSearchUpdate?.Invoke(update);
|
||||
});
|
||||
|
||||
_hubConnection.On<StatusUpdate>("statusUpdate", update =>
|
||||
{
|
||||
_logger.LogDebug("Received statusUpdate: {Message}", update.Message);
|
||||
OnStatusUpdate?.Invoke(update);
|
||||
});
|
||||
|
||||
// Handle reconnection events
|
||||
_hubConnection.Reconnected += connectionId =>
|
||||
{
|
||||
_logger.LogInformation("SignalR reconnected: {ConnectionId}", connectionId);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Reconnecting += error =>
|
||||
{
|
||||
_logger.LogWarning(error, "SignalR reconnecting...");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Closed += error =>
|
||||
{
|
||||
_logger.LogWarning(error, "SignalR connection closed");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
_logger.LogInformation("Starting SignalR connection...");
|
||||
await _hubConnection.StartAsync(ct);
|
||||
_logger.LogInformation("SignalR connected");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.StopAsync(ct);
|
||||
_logger.LogInformation("SignalR disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StatusUpdate?> GetCachedStatusAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
return await _hubConnection.InvokeAsync<StatusUpdate?>("GetCachedStatus", ct);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _hubConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SignalR Message Types
|
||||
|
||||
```csharp
|
||||
public record SearchUpdate(
|
||||
int ID,
|
||||
string Name,
|
||||
string UserName,
|
||||
SearchStatus Status,
|
||||
DateTime? SubmitDT,
|
||||
DateTime? StartDT,
|
||||
DateTime? EndDT);
|
||||
|
||||
public record StatusUpdate(
|
||||
string Message,
|
||||
DateTime Timestamp);
|
||||
```
|
||||
|
||||
## API Client Services
|
||||
|
||||
### ISearchService
|
||||
|
||||
```csharp
|
||||
public interface ISearchService
|
||||
{
|
||||
Task<List<SearchViewModel>> GetUserSearchesAsync(CancellationToken ct = default);
|
||||
Task<SearchViewModel> GetSearchAsync(int id, CancellationToken ct = default);
|
||||
Task<SearchViewModel> CopySearchAsync(int id, CancellationToken ct = default);
|
||||
Task<int> SaveSearchAsync(SearchViewModel search, CancellationToken ct = default);
|
||||
Task<List<SearchViewModel>> GetQueueAsync(CancellationToken ct = default);
|
||||
Task DownloadResultsAsync(int searchId, CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
### ILookupService
|
||||
|
||||
```csharp
|
||||
public interface ILookupService
|
||||
{
|
||||
Task<IEnumerable<ItemViewModel>> FindItemsAsync(string query, CancellationToken ct = default);
|
||||
Task<IEnumerable<ProfitCenterViewModel>> FindProfitCentersAsync(string query, CancellationToken ct = default);
|
||||
Task<IEnumerable<WorkCenterViewModel>> FindWorkCentersAsync(string query, CancellationToken ct = default);
|
||||
Task<IEnumerable<OperatorViewModel>> FindOperatorsAsync(string query, CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
### IFileService
|
||||
|
||||
```csharp
|
||||
public interface IFileService
|
||||
{
|
||||
Task DownloadTemplateAsync(string templateType, CancellationToken ct = default);
|
||||
Task<FileUploadResult<T>> UploadAsync<T>(IBrowserFile file, string endpoint, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public record FileUploadResult<T>(
|
||||
bool WasSuccessful,
|
||||
string? ErrorMessage,
|
||||
List<T>? Data);
|
||||
```
|
||||
|
||||
## File Download via JS Interop
|
||||
|
||||
### wwwroot/js/interop.js
|
||||
|
||||
```javascript
|
||||
window.downloadFileFromStream = async (fileName, streamRef) => {
|
||||
const arrayBuffer = await streamRef.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName ?? '';
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
window.downloadFileFromUrl = (url, fileName) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName ?? '';
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
};
|
||||
```
|
||||
|
||||
### FileService Download Implementation
|
||||
|
||||
```csharp
|
||||
public async Task DownloadResultsAsync(int searchId, CancellationToken ct)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/search/{searchId}/results", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var fileName = $"Search_{searchId}_Results.xlsx";
|
||||
var content = await response.Content.ReadAsStreamAsync(ct);
|
||||
|
||||
using var streamRef = new DotNetStreamReference(content);
|
||||
await _jsRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
|
||||
}
|
||||
```
|
||||
|
||||
## Filter Panel Component Pattern
|
||||
|
||||
### Base Structure
|
||||
|
||||
All filter panels follow a consistent pattern with:
|
||||
- Card container with title
|
||||
- Optional toolbar (Download Template, Upload Data, Clear Data)
|
||||
- Input controls (autocomplete, date picker, etc.)
|
||||
- Data grid of selected items
|
||||
- Count display
|
||||
|
||||
### Two-Way Binding Pattern
|
||||
|
||||
```razor
|
||||
@* ItemNumberFilterPanel.razor - simplified *@
|
||||
<RadzenCard>
|
||||
<RadzenStack Gap="1rem">
|
||||
<RadzenText TextStyle="TextStyle.H6" Text="Filter by item number" />
|
||||
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<RadzenRow Gap="1rem" AlignItems="AlignItems.End">
|
||||
<RadzenColumn Size="8">
|
||||
<RadzenAutoComplete @bind-Value="@selectedItemText"
|
||||
Data="@itemSuggestions"
|
||||
LoadData="@LoadItemSuggestions"
|
||||
MinLength="3" />
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="4">
|
||||
<RadzenButton Text="Add to filter" Click="@HandleAddItem" />
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
}
|
||||
|
||||
<RadzenDataGrid Data="@Items" TItem="ItemViewModel">
|
||||
<Columns>
|
||||
<RadzenDataGridColumn Property="ItemNumber" Title="Item Number" />
|
||||
<RadzenDataGridColumn Property="Description" Title="Description" />
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<RadzenDataGridColumn Title="Actions" Width="100px">
|
||||
<Template Context="item">
|
||||
<RadzenButton Icon="delete" Click="@(() => HandleDeleteItem(item))" />
|
||||
</Template>
|
||||
</RadzenDataGridColumn>
|
||||
}
|
||||
</Columns>
|
||||
</RadzenDataGrid>
|
||||
|
||||
<RadzenText Text="@($"# of item numbers: {Items.Count}")" />
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<ItemViewModel> Items { get; set; } = new();
|
||||
[Parameter] public EventCallback<List<ItemViewModel>> ItemsChanged { get; set; }
|
||||
[Parameter] public bool IsReadOnly { get; set; }
|
||||
|
||||
private async Task HandleAddItem()
|
||||
{
|
||||
// Add item and notify parent
|
||||
Items.Add(new ItemViewModel { ... });
|
||||
await ItemsChanged.InvokeAsync(Items);
|
||||
}
|
||||
|
||||
private async Task HandleDeleteItem(ItemViewModel item)
|
||||
{
|
||||
Items.Remove(item);
|
||||
await ItemsChanged.InvokeAsync(Items);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Global Error Boundary
|
||||
|
||||
```razor
|
||||
@* In MainLayout.razor *@
|
||||
<ErrorBoundary @ref="errorBoundary">
|
||||
<ChildContent>
|
||||
@Body
|
||||
</ChildContent>
|
||||
<ErrorContent Context="exception">
|
||||
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true">
|
||||
An error occurred. Please try again or contact support.
|
||||
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
|
||||
{
|
||||
<pre>@exception.Message</pre>
|
||||
}
|
||||
</RadzenAlert>
|
||||
</ErrorContent>
|
||||
</ErrorBoundary>
|
||||
|
||||
@code {
|
||||
private ErrorBoundary? errorBoundary;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
errorBoundary?.Recover();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service-Level Error Handling
|
||||
|
||||
```csharp
|
||||
public class SearchService : ISearchService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly ILogger<SearchService> _logger;
|
||||
|
||||
public async Task<List<SearchViewModel>> GetUserSearchesAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<List<SearchViewModel>>(
|
||||
"/api/search", ct) ?? new();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch user searches");
|
||||
_notificationService.Notify(NotificationSeverity.Error,
|
||||
"Error", "Failed to load searches. Please try again.");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
### Component-Level Loading
|
||||
|
||||
```razor
|
||||
@if (isLoading)
|
||||
{
|
||||
<RadzenStack AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center"
|
||||
Style="min-height: 200px;">
|
||||
<RadzenProgressBarCircular Mode="ProgressBarMode.Indeterminate" Size="60" />
|
||||
<RadzenText Text="Loading..." />
|
||||
</RadzenStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Content *@
|
||||
}
|
||||
```
|
||||
|
||||
### Grid-Level Loading
|
||||
|
||||
```razor
|
||||
<RadzenDataGrid TItem="SearchViewModel" Data="@searches"
|
||||
IsLoading="@isLoading" Style="min-height: 450px;">
|
||||
```
|
||||
|
||||
## Radzen Component Reference
|
||||
|
||||
| UI Element | Component | Key Properties |
|
||||
|------------|-----------|----------------|
|
||||
| Layout container | RadzenLayout | RadzenHeader, RadzenBody, RadzenFooter |
|
||||
| Navigation | RadzenLink, NavigationManager | Path, NavigateTo |
|
||||
| Data grid | RadzenDataGrid<T> | AllowPaging, AllowSorting, IsLoading |
|
||||
| Dropdown | RadzenDropDown<T> | Data, TextProperty, ValueProperty |
|
||||
| Autocomplete | RadzenAutoComplete | LoadData, MinLength |
|
||||
| Date picker | RadzenDatePicker | DateFormat, Min, Max |
|
||||
| Text input | RadzenTextBox | Placeholder, Disabled |
|
||||
| Password | RadzenPassword | Placeholder |
|
||||
| Button | RadzenButton | Text, Icon, ButtonStyle, IsBusy |
|
||||
| Card | RadzenCard | - |
|
||||
| Alert | RadzenAlert | AlertStyle, ShowIcon |
|
||||
| Badge | RadzenBadge | BadgeStyle, Text |
|
||||
| Progress | RadzenProgressBarCircular | Mode="Indeterminate" |
|
||||
| Dialog | DialogService | Confirm, Alert |
|
||||
| Notification | NotificationService | Notify |
|
||||
| File upload | RadzenUpload | Url, Accept, Complete |
|
||||
| Checkbox | RadzenCheckBox | @bind-Value |
|
||||
| Validation | EditForm + DataAnnotationsValidator | OnValidSubmit |
|
||||
|
||||
## NuGet Dependencies
|
||||
|
||||
The JdeScoping.Client project already includes required packages:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.*" />
|
||||
<PackageReference Include="Radzen.Blazor" Version="8.*" />
|
||||
</ItemGroup>
|
||||
```
|
||||
@@ -0,0 +1,111 @@
|
||||
# Implement Blazor UI
|
||||
|
||||
## Summary
|
||||
|
||||
Implement the Blazor WebAssembly user interface for the JDE Scoping Tool, migrating the legacy ASP.NET MVC 5 / Kendo UI implementation to modern Radzen Blazor components. This phase creates all pages (Login, Search List, Search Create/Edit, Search Queue, Refresh Status) and integrates SignalR for real-time status updates.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Blazor WebAssembly project setup and configuration (already exists as JdeScoping.Client)
|
||||
- Radzen Blazor component library integration (already referenced)
|
||||
- Main layout with navigation header and footer
|
||||
- Authentication pages:
|
||||
- Login page with LDAP authentication form
|
||||
- Not Authorized page
|
||||
- Search pages:
|
||||
- Search List page (user's searches with status badges)
|
||||
- Search Create/Edit page with filter panels
|
||||
- Search Queue page (all queued searches)
|
||||
- Refresh Status page (data sync dashboard)
|
||||
- Filter panel components:
|
||||
- TimeSpan Filter Panel
|
||||
- Work Order Filter Panel
|
||||
- Item Number Filter Panel
|
||||
- Profit Center Filter Panel
|
||||
- Work Center Filter Panel
|
||||
- Component Lot Filter Panel
|
||||
- Operator Filter Panel
|
||||
- Part/Operation/MIS Filter Panel
|
||||
- SignalR client service for real-time updates
|
||||
- API client services:
|
||||
- IAuthService
|
||||
- ISearchService
|
||||
- ILookupService
|
||||
- IFileService
|
||||
- IRefreshStatusService
|
||||
- ValidCombination model (search type definitions)
|
||||
- State management via AuthStateProvider
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Backend API implementation (Phase 8: web-api-auth)
|
||||
- SignalR hub implementation (Phase 8: web-api-auth)
|
||||
- Unit tests for Blazor components (bUnit testing deferred)
|
||||
- Mobile-responsive design optimizations
|
||||
- Offline/PWA functionality
|
||||
- Dark mode theming
|
||||
|
||||
## Motivation
|
||||
|
||||
The legacy ASP.NET MVC 5 / Kendo UI implementation has several limitations:
|
||||
|
||||
- **License cost**: Kendo UI requires commercial license
|
||||
- **Framework obsolescence**: ASP.NET MVC 5 is not actively developed
|
||||
- **JavaScript complexity**: Heavy jQuery-based scripting
|
||||
- **Page reload model**: Full page reloads for navigation
|
||||
|
||||
Blazor WebAssembly with Radzen provides:
|
||||
|
||||
- **Free tier**: Radzen Blazor community edition is open source
|
||||
- **Modern framework**: .NET 10 with full C# component model
|
||||
- **Type safety**: C# throughout, no JavaScript required for core functionality
|
||||
- **SPA experience**: No page reloads, smooth navigation
|
||||
- **SignalR integration**: Native .NET SignalR client
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. All pages render correctly in modern browsers (Chrome, Firefox, Edge, Safari)
|
||||
2. Login page authenticates via backend API (mocked initially)
|
||||
3. Search List page displays user's searches with real-time status updates
|
||||
4. Search Create/Edit page:
|
||||
- Search type dropdown controls filter panel visibility
|
||||
- All 8 filter panel types functional
|
||||
- Form validation prevents submission without required filters
|
||||
- Read-only mode for submitted searches with Copy button
|
||||
5. Search Queue page displays all queued searches with processor status
|
||||
6. Refresh Status page displays data sync history with date filtering
|
||||
7. SignalR connection:
|
||||
- Connects on page load
|
||||
- Receives searchUpdate and statusUpdate events
|
||||
- Reconnects automatically on disconnect
|
||||
8. All file upload/download flows functional (templates, data upload, results download)
|
||||
9. Navigation is same-window (no popups or new tabs)
|
||||
10. `openspec validate implement-blazor-ui --strict` passes
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Phase 8**: implement-web-api-auth - Backend API endpoints for authentication, search operations, file handling
|
||||
- **JdeScoping.Client project**: Already created with Blazor WASM and Radzen packages
|
||||
- **NuGet packages**:
|
||||
- `Radzen.Blazor` Version="8.*" (already referenced)
|
||||
- `Microsoft.AspNetCore.SignalR.Client` Version="10.*" (already referenced)
|
||||
- `Microsoft.AspNetCore.Components.WebAssembly` Version="10.*" (already referenced)
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| API not ready | Mock services return static/test data until Phase 8 complete |
|
||||
| SignalR connection issues | Implement robust reconnection with exponential backoff |
|
||||
| File download in WASM | Use JS interop for browser download trigger |
|
||||
| Large filter lists | Implement virtualization for autocomplete dropdowns |
|
||||
| Component complexity | Extract reusable filter panel base component |
|
||||
| Radzen version changes | Pin to specific minor version, test upgrades |
|
||||
|
||||
## Related Specs
|
||||
|
||||
- `web-ui` - Base specification for UI components and pages
|
||||
- `web-api-auth` - API endpoints that UI will consume
|
||||
- `domain-models` - ViewModels used in UI components
|
||||
@@ -0,0 +1,328 @@
|
||||
# Web UI Specification Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
This document captures ADDED and MODIFIED requirements for the Blazor WebAssembly user interface specific to the .NET 10 migration. It supplements the base specification at `openspec/specs/web-ui/spec.md`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Blazor WebAssembly Hosting Model
|
||||
|
||||
The system SHALL use Blazor WebAssembly (WASM) as the client-side hosting model.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- The client application SHALL run entirely in the browser via WebAssembly
|
||||
- The application SHALL communicate with the server exclusively via HTTP APIs and SignalR
|
||||
- The application SHALL NOT use server-side Blazor (SignalR for DOM updates)
|
||||
- Initial load SHALL include the .NET runtime and application assemblies
|
||||
- Subsequent navigation SHALL NOT require page reloads
|
||||
|
||||
#### Scenario: Initial application load
|
||||
|
||||
- **WHEN** a user navigates to the application URL
|
||||
- **THEN** the browser downloads the .NET WASM runtime
|
||||
- **AND** the Blazor application initializes in the browser
|
||||
- **AND** subsequent interactions do not trigger server-side rendering
|
||||
|
||||
---
|
||||
|
||||
### Requirement: JWT Token Authentication
|
||||
|
||||
The system SHALL use JWT tokens for API authentication stored in browser localStorage.
|
||||
|
||||
#### Inputs
|
||||
|
||||
- Login credentials (username, password)
|
||||
- LDAP authentication endpoint
|
||||
|
||||
#### Outputs
|
||||
|
||||
- JWT token stored in localStorage
|
||||
- Claims extracted for AuthenticationState
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- Tokens SHALL be stored in browser localStorage via JS interop
|
||||
- Tokens SHALL be automatically attached to outgoing HTTP requests
|
||||
- Token expiration SHALL trigger re-authentication prompt
|
||||
- Logout SHALL remove token from localStorage and clear auth state
|
||||
|
||||
#### Scenario: Login stores JWT token
|
||||
|
||||
- **WHEN** user submits valid credentials
|
||||
- **THEN** the API returns a JWT token
|
||||
- **AND** the token is stored in localStorage under key "authToken"
|
||||
- **AND** AuthStateProvider parses claims from the token
|
||||
- **AND** subsequent API requests include Authorization header
|
||||
|
||||
#### Scenario: Token expiration
|
||||
|
||||
- **WHEN** a stored token has expired
|
||||
- **AND** the user attempts an API call
|
||||
- **THEN** the API returns 401 Unauthorized
|
||||
- **AND** the user is redirected to the login page
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Custom AuthenticationStateProvider
|
||||
|
||||
The system SHALL implement a custom AuthenticationStateProvider for JWT-based authentication state.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- AuthStateProvider SHALL parse JWT claims without server round-trip
|
||||
- Claims SHALL include username, roles, and expiration
|
||||
- State changes SHALL notify Blazor components via NotifyAuthenticationStateChanged
|
||||
- Invalid/expired tokens SHALL result in anonymous state
|
||||
|
||||
#### Scenario: Parse claims from JWT
|
||||
|
||||
- **WHEN** AuthStateProvider initializes with a stored token
|
||||
- **THEN** it parses the token payload (base64-decoded JSON)
|
||||
- **AND** extracts claims into ClaimsPrincipal
|
||||
- **AND** sets authentication type to "jwt"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SignalR Auto-Reconnect
|
||||
|
||||
The system SHALL implement automatic reconnection for SignalR connections with exponential backoff.
|
||||
|
||||
#### Reconnection Schedule
|
||||
|
||||
| Attempt | Delay |
|
||||
|---------|-------|
|
||||
| 1 | 0 seconds |
|
||||
| 2 | 2 seconds |
|
||||
| 3 | 5 seconds |
|
||||
| 4 | 10 seconds |
|
||||
| 5 | 30 seconds |
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- SignalR client SHALL use WithAutomaticReconnect configuration
|
||||
- Reconnection attempts SHALL follow exponential backoff schedule
|
||||
- UI SHALL indicate connection state during reconnection
|
||||
- Events received during reconnection SHALL be delivered after reconnect
|
||||
|
||||
#### Scenario: Network interruption recovery
|
||||
|
||||
- **WHEN** the SignalR connection is lost
|
||||
- **THEN** the client attempts reconnection per the backoff schedule
|
||||
- **AND** logs reconnection attempts to console
|
||||
- **AND** upon successful reconnection, resumes receiving events
|
||||
|
||||
---
|
||||
|
||||
### Requirement: File Download via JS Interop
|
||||
|
||||
The system SHALL use JavaScript interop for triggering browser file downloads.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- Excel result files SHALL be downloaded via JS interop function
|
||||
- Template files SHALL be downloaded via direct URL navigation
|
||||
- Downloaded files SHALL prompt browser save dialog
|
||||
- File names SHALL be specified by the server response headers
|
||||
|
||||
#### JavaScript Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `downloadFileFromStream` | Download file from DotNetStreamReference |
|
||||
| `downloadFileFromUrl` | Download file from URL with filename |
|
||||
|
||||
#### Scenario: Download search results
|
||||
|
||||
- **WHEN** user clicks Download Results button
|
||||
- **THEN** API request fetches file as stream
|
||||
- **AND** JS interop triggers browser download dialog
|
||||
- **AND** file is saved with name "Search_{id}_Results.xlsx"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Radzen Component Library Integration
|
||||
|
||||
The system SHALL use Radzen Blazor (free tier) for UI components.
|
||||
|
||||
#### Service Registration
|
||||
|
||||
- DialogService SHALL be registered for confirmation dialogs
|
||||
- NotificationService SHALL be registered for toast notifications
|
||||
- RadzenComponents SHALL be registered via AddRadzenComponents()
|
||||
|
||||
#### CSS and JavaScript
|
||||
|
||||
- Radzen CSS SHALL be included in index.html
|
||||
- No additional Radzen JavaScript required for core components
|
||||
|
||||
#### Scenario: Register Radzen services
|
||||
|
||||
- **WHEN** the application starts
|
||||
- **THEN** DialogService is available for injection
|
||||
- **AND** NotificationService is available for injection
|
||||
- **AND** Radzen component styles are applied
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Async-First Service Design
|
||||
|
||||
The system SHALL use async methods for all service operations.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- All HTTP client calls SHALL use async methods (GetFromJsonAsync, PostAsJsonAsync)
|
||||
- All service interfaces SHALL return Task or Task<T>
|
||||
- Cancellation tokens SHALL be accepted on all service methods
|
||||
- UI SHALL remain responsive during API calls
|
||||
|
||||
#### Scenario: Async API call with loading state
|
||||
|
||||
- **WHEN** user triggers a data load operation
|
||||
- **THEN** loading indicator displays immediately
|
||||
- **AND** API call executes asynchronously
|
||||
- **AND** UI updates when data arrives
|
||||
- **AND** loading indicator hides
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ILogger Client-Side Logging
|
||||
|
||||
The system SHALL use Microsoft.Extensions.Logging for client-side logging.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- All services SHALL accept ILogger<T> via constructor injection
|
||||
- Log levels SHALL be configurable in Program.cs
|
||||
- Logs SHALL output to browser console in development
|
||||
- Error logs SHALL include exception details
|
||||
|
||||
#### Log Levels by Category
|
||||
|
||||
| Category | Minimum Level |
|
||||
|----------|---------------|
|
||||
| Default | Information |
|
||||
| Microsoft.AspNetCore | Warning |
|
||||
| SignalR | Debug (dev) / Information (prod) |
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Same-Window Navigation
|
||||
|
||||
The system SHALL use same-window navigation for all internal links (no popups or new tabs).
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- All internal navigation SHALL use NavigationManager.NavigateTo
|
||||
- Search detail links SHALL NOT open new windows
|
||||
- Queue links SHALL NOT open new tabs
|
||||
- Only external links MAY open new tabs (if any exist)
|
||||
|
||||
#### Scenario: Navigate to search detail
|
||||
|
||||
- **WHEN** user clicks View button on search grid row
|
||||
- **THEN** NavigationManager.NavigateTo("/search/{id}") is called
|
||||
- **AND** current window navigates to search detail
|
||||
- **AND** no new window or tab opens
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Clear Data Confirmation
|
||||
|
||||
The system SHALL display confirmation dialogs before clearing filter data.
|
||||
|
||||
#### Inputs
|
||||
|
||||
- User clicks Clear Data button on any filter panel
|
||||
|
||||
#### Outputs
|
||||
|
||||
- Confirmation dialog with OK/Cancel buttons
|
||||
- Data cleared only if user confirms
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- DialogService.Confirm SHALL be used for confirmation dialogs
|
||||
- Dialog title SHALL be "Confirm Clear"
|
||||
- Dialog message SHALL be "Are you sure you want to clear all items?"
|
||||
- Cancel SHALL leave data unchanged
|
||||
|
||||
#### Scenario: Clear filter with confirmation
|
||||
|
||||
- **WHEN** user clicks Clear Data button
|
||||
- **THEN** confirmation dialog appears
|
||||
- **AND** if user clicks OK, filter list is cleared
|
||||
- **AND** if user clicks Cancel, filter list is unchanged
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Operator Filter Display
|
||||
|
||||
The system SHALL display Operator filter entries with AddressNumber, UserID, and FullName properties.
|
||||
|
||||
#### Display Format
|
||||
|
||||
| Column | Property | Width |
|
||||
|--------|----------|-------|
|
||||
| Address Number | AddressNumber | 120px |
|
||||
| User ID | UserID | 100px |
|
||||
| Full Name | FullName | Auto |
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- Autocomplete dropdown SHALL display all three properties
|
||||
- Format: "{AddressNumber} - {UserID} - {FullName}"
|
||||
- Grid SHALL display all three properties in separate columns
|
||||
|
||||
#### Scenario: Select operator from autocomplete
|
||||
|
||||
- **WHEN** user types in operator autocomplete
|
||||
- **THEN** dropdown shows results with format "12345 - JSMITH - John Smith"
|
||||
- **AND** selecting adds entry with all three properties to grid
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error Handling Without Custom Error Pages
|
||||
|
||||
The system SHALL use Blazor's built-in error handling instead of custom error pages.
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- ErrorBoundary component SHALL wrap page content in MainLayout
|
||||
- Unhandled exceptions SHALL display inline error message
|
||||
- Error recovery SHALL be automatic on next navigation
|
||||
- Development mode SHALL show exception details
|
||||
- Production mode SHALL show generic error message
|
||||
|
||||
#### Scenario: Unhandled exception in component
|
||||
|
||||
- **WHEN** an unhandled exception occurs in a Blazor component
|
||||
- **THEN** ErrorBoundary catches the exception
|
||||
- **AND** error content displays instead of the faulted component
|
||||
- **AND** other components remain functional
|
||||
- **AND** navigating away recovers the error boundary
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
| Legacy Pattern | New Pattern | Status |
|
||||
|----------------|-------------|--------|
|
||||
| Kendo UI Grid | RadzenDataGrid | ADDED |
|
||||
| Kendo DatePicker | RadzenDatePicker | ADDED |
|
||||
| Kendo ComboBox | RadzenAutoComplete | ADDED |
|
||||
| jQuery AJAX | HttpClient | ADDED |
|
||||
| Forms Authentication | JWT with localStorage | MODIFIED |
|
||||
| SignalR (jQuery) | SignalR (.NET client) | ADDED |
|
||||
| New window navigation | Same-window navigation | MODIFIED |
|
||||
| Custom error pages | ErrorBoundary | MODIFIED |
|
||||
| Clear without confirm | Clear with DialogService.Confirm | MODIFIED |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
None - all design decisions resolved per best practice recommendations.
|
||||
@@ -0,0 +1,307 @@
|
||||
# Tasks: Implement Blazor UI
|
||||
|
||||
## Phase 1: Project Configuration
|
||||
|
||||
- [x] 001: Verify JdeScoping.Client project configuration
|
||||
- Location: `NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
||||
- Verify: Radzen.Blazor, SignalR.Client packages referenced
|
||||
- Validation: `dotnet build` succeeds
|
||||
|
||||
- [x] 002: Configure _Imports.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/_Imports.razor`
|
||||
- Add: Radzen, SignalR, Models, Services namespaces
|
||||
- Validation: Namespace imports resolve
|
||||
|
||||
- [x] 003: Update App.razor with routing
|
||||
- Location: `NEW/src/JdeScoping.Client/App.razor`
|
||||
- Add: CascadingAuthenticationState, AuthorizeRouteView
|
||||
- Validation: Router configuration compiles
|
||||
|
||||
- [x] 004: Configure Program.cs service registration
|
||||
- Location: `NEW/src/JdeScoping.Client/Program.cs`
|
||||
- Add: Radzen services, Auth services, Application services
|
||||
- Validation: Application starts without DI errors
|
||||
|
||||
- [x] 005: Add JS interop file for file downloads
|
||||
- Location: `NEW/src/JdeScoping.Client/wwwroot/js/interop.js`
|
||||
- Add: downloadFileFromStream, downloadFileFromUrl functions
|
||||
- Validation: JS file loads in browser
|
||||
|
||||
## Phase 2: Models
|
||||
|
||||
- [x] 006: Create ValidCombination model
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/ValidCombination.cs`
|
||||
- Source: `OLD/WebInterface/Scripts/model/models.js` (ValidCombination definitions)
|
||||
- Include: All 16 search type combinations with filter flags
|
||||
- Validation: GetAll() returns 16 items
|
||||
|
||||
- [x] 007: Create LoginModel
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/LoginModel.cs`
|
||||
- Properties: Username (required), Password (required)
|
||||
- Include: DataAnnotations validation attributes
|
||||
- Validation: Validation fails for empty fields
|
||||
|
||||
- [x] 008: Create SearchViewModel
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/SearchViewModel.cs`
|
||||
- Properties: ID, Name, UserName, Status, SubmitDT, StartDT, EndDT, Criteria
|
||||
- Validation: Model compiles with all properties
|
||||
|
||||
- [x] 009: Create SearchCriteriaViewModel
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/SearchCriteriaViewModel.cs`
|
||||
- Properties: MinimumDT, MaximumDT, all filter collections, ExtractMisData
|
||||
- Validation: Model compiles with all properties
|
||||
|
||||
- [x] 010: Create filter item ViewModels
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/`
|
||||
- Files: ItemViewModel, ProfitCenterViewModel, WorkCenterViewModel, OperatorViewModel, WorkOrderViewModel, ComponentLotViewModel, PartOperationViewModel
|
||||
- Validation: All models compile
|
||||
|
||||
- [x] 011: Create SignalR message models
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/`
|
||||
- Files: SearchUpdate.cs, StatusUpdate.cs
|
||||
- Validation: Records compile
|
||||
|
||||
- [x] 012: Create DataUpdateViewModel
|
||||
- Location: `NEW/src/JdeScoping.Client/Models/DataUpdateViewModel.cs`
|
||||
- Properties: StartDT, EndDT, record counts for each table, WasSuccessful
|
||||
- Validation: Model compiles
|
||||
|
||||
## Phase 3: Authentication Services
|
||||
|
||||
- [x] 013: Create ITokenStorageService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Auth/ITokenStorageService.cs`
|
||||
- Methods: GetTokenAsync, SetTokenAsync, RemoveTokenAsync
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 014: Create TokenStorageService
|
||||
- Location: `NEW/src/JdeScoping.Client/Auth/TokenStorageService.cs`
|
||||
- Implementation: localStorage via IJSRuntime
|
||||
- Validation: Can store/retrieve/remove token
|
||||
|
||||
- [x] 015: Create AuthStateProvider
|
||||
- Location: `NEW/src/JdeScoping.Client/Auth/AuthStateProvider.cs`
|
||||
- Implementation: Parse JWT claims, manage auth state
|
||||
- Methods: GetAuthenticationStateAsync, NotifyAuthenticationStateChangedAsync, LogoutAsync
|
||||
- Validation: Auth state changes propagate to components
|
||||
|
||||
- [x] 016: Create IAuthService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/IAuthService.cs`
|
||||
- Methods: LoginAsync, LogoutAsync
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 017: Create AuthService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/AuthService.cs`
|
||||
- Implementation: Call /api/auth/login, store token
|
||||
- Returns: AuthResult with Success, ErrorMessage, Token
|
||||
- Validation: Login flow works with mock endpoint
|
||||
|
||||
## Phase 4: SignalR Service
|
||||
|
||||
- [x] 018: Create IHubConnectionService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/IHubConnectionService.cs`
|
||||
- Methods: StartAsync, StopAsync, GetCachedStatusAsync
|
||||
- Events: OnSearchUpdate, OnStatusUpdate
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 019: Create HubConnectionService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/HubConnectionService.cs`
|
||||
- Implementation: HubConnectionBuilder with auto-reconnect
|
||||
- Subscribe: searchUpdate, statusUpdate events
|
||||
- Validation: Connects to /hubs/status, receives events
|
||||
|
||||
## Phase 5: API Client Services
|
||||
|
||||
- [x] 020: Create ISearchService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/ISearchService.cs`
|
||||
- Methods: GetUserSearchesAsync, GetSearchAsync, CopySearchAsync, SaveSearchAsync, GetQueueAsync, DownloadResultsAsync
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 021: Create SearchService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/SearchService.cs`
|
||||
- Implementation: HttpClient calls to /api/search endpoints
|
||||
- Include: Error handling, logging
|
||||
- Validation: Service compiles, handles errors gracefully
|
||||
|
||||
- [x] 022: Create ILookupService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/ILookupService.cs`
|
||||
- Methods: FindItemsAsync, FindProfitCentersAsync, FindWorkCentersAsync, FindOperatorsAsync
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 023: Create LookupService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/LookupService.cs`
|
||||
- Implementation: HttpClient calls to /api/lookup endpoints
|
||||
- Validation: Service compiles
|
||||
|
||||
- [x] 024: Create IFileService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/IFileService.cs`
|
||||
- Methods: DownloadTemplateAsync, DownloadPartNumberTemplateAsync, UploadAsync
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 025: Create FileService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/FileService.cs`
|
||||
- Implementation: File download via JS interop, upload via RadzenUpload
|
||||
- Validation: Download triggers browser save dialog
|
||||
|
||||
- [x] 026: Create IRefreshStatusService interface
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/IRefreshStatusService.cs`
|
||||
- Methods: GetRefreshStatusAsync(minDT, maxDT)
|
||||
- Validation: Interface compiles
|
||||
|
||||
- [x] 027: Create RefreshStatusService
|
||||
- Location: `NEW/src/JdeScoping.Client/Services/RefreshStatusService.cs`
|
||||
- Implementation: HttpClient calls to /api/refresh-status
|
||||
- Validation: Service compiles
|
||||
|
||||
## Phase 6: Layout Components
|
||||
|
||||
- [x] 028: Create MainLayout.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Layout/MainLayout.razor`
|
||||
- Structure: RadzenLayout with Header, Body, Footer
|
||||
- Include: AuthorizeView for user display, logout button
|
||||
- Validation: Layout renders header, content, footer
|
||||
|
||||
- [x] 029: Create LoadingIndicator component
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/Shared/LoadingIndicator.razor`
|
||||
- Structure: RadzenProgressBarCircular with optional message
|
||||
- Validation: Component renders centered spinner
|
||||
|
||||
## Phase 7: Authentication Pages
|
||||
|
||||
- [x] 030: Create Login.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/Login.razor`
|
||||
- Route: /login
|
||||
- Structure: RadzenCard with EditForm, username/password fields
|
||||
- Features: Validation, error display, loading state, redirect on success
|
||||
- Validation: Login form submits and redirects
|
||||
|
||||
- [x] 031: Create NotAuthorized.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/NotAuthorized.razor`
|
||||
- Route: /not-authorized
|
||||
- Structure: RadzenAlert with error message, navigation buttons
|
||||
- Validation: Page displays resource URL from query string
|
||||
|
||||
- [x] 032: Create RedirectToLogin component
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/Shared/RedirectToLogin.razor`
|
||||
- Implementation: NavigateTo /login with returnUrl
|
||||
- Validation: Unauthorized access redirects to login
|
||||
|
||||
## Phase 8: Search List Page
|
||||
|
||||
- [x] 033: Create Searches.razor (Search List)
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/Searches.razor`
|
||||
- Routes: / and /searches
|
||||
- Structure: RadzenDataGrid with Name, Submitted, Status columns
|
||||
- Features: New Search button, Queue button, status badges
|
||||
- SignalR: Subscribe to searchUpdate, update grid in real-time
|
||||
- Validation: Grid displays user's searches, updates on SignalR events
|
||||
|
||||
## Phase 9: Search Create/Edit Page
|
||||
|
||||
- [x] 034: Create SearchEdit.razor (main page)
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/SearchEdit.razor`
|
||||
- Routes: /search/create, /search/{Id:int}
|
||||
- Structure: Search details panel, conditional filter panels
|
||||
- Features: Search type dropdown, read-only mode, Copy button, Submit button
|
||||
- SignalR: Subscribe to searchUpdate for current search
|
||||
- Validation: Form loads, validates, submits correctly
|
||||
|
||||
## Phase 10: Filter Panel Components
|
||||
|
||||
- [x] 035: Create TimeSpanFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/TimeSpanFilterPanel.razor`
|
||||
- Structure: Min/Max date pickers
|
||||
- Business rules: Min >= 2002-11-01, Max <= today, Max >= Min
|
||||
- Validation: Date constraints enforced
|
||||
|
||||
- [x] 036: Create WorkOrderFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/WorkOrderFilterPanel.razor`
|
||||
- Structure: Upload/Download/Clear buttons, data grid
|
||||
- Features: Excel upload parsing, template download
|
||||
- Validation: Upload populates grid, Clear empties grid
|
||||
|
||||
- [x] 037: Create ItemNumberFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor`
|
||||
- Structure: Autocomplete with Add button, data grid
|
||||
- Features: Search with 3+ chars, prevent duplicates
|
||||
- Validation: Autocomplete returns results, Add works
|
||||
|
||||
- [x] 038: Create ProfitCenterFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor`
|
||||
- Structure: Autocomplete with Add button, data grid
|
||||
- Validation: Same pattern as ItemNumber panel
|
||||
|
||||
- [x] 039: Create WorkCenterFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor`
|
||||
- Structure: Autocomplete with Add button, data grid
|
||||
- Validation: Same pattern as ItemNumber panel
|
||||
|
||||
- [x] 040: Create OperatorFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor`
|
||||
- Structure: Autocomplete with Add button, data grid
|
||||
- Properties: AddressNumber, UserID, FullName
|
||||
- Validation: Displays all three properties in dropdown
|
||||
|
||||
- [x] 041: Create ComponentLotFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/ComponentLotFilterPanel.razor`
|
||||
- Structure: Upload/Download/Clear buttons, data grid
|
||||
- Properties: LotNumber, ItemNumber
|
||||
- Validation: Two-column display in grid
|
||||
|
||||
- [x] 042: Create PartOperationFilterPanel.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Components/FilterPanels/PartOperationFilterPanel.razor`
|
||||
- Structure: Upload/Download/Clear buttons, data grid
|
||||
- Properties: ItemNumber, OperationNumber, MisNumber, MisRevision
|
||||
- Validation: Four-column display in grid
|
||||
|
||||
## Phase 11: Search Queue Page
|
||||
|
||||
- [x] 043: Create SearchQueue.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/SearchQueue.razor`
|
||||
- Route: /search/queue
|
||||
- Structure: Processor status panel, data grid of all queued searches
|
||||
- SignalR: Subscribe to statusUpdate, searchUpdate
|
||||
- Features: Remove completed searches from grid
|
||||
- Validation: Grid shows all users' searches, status panel updates
|
||||
|
||||
## Phase 12: Refresh Status Page
|
||||
|
||||
- [x] 044: Create RefreshStatus.razor
|
||||
- Location: `NEW/src/JdeScoping.Client/Pages/RefreshStatus.razor`
|
||||
- Route: /refresh-status
|
||||
- Structure: Date filter panel, data grid with record counts
|
||||
- Features: Default to last 7 days, Filter button, WasSuccessful badges
|
||||
- Validation: Grid displays sync history, filtering works
|
||||
|
||||
## Phase 13: Styling and Polish
|
||||
|
||||
- [x] 045: Update wwwroot/css/app.css
|
||||
- Location: `NEW/src/JdeScoping.Client/wwwroot/css/app.css`
|
||||
- Add: Custom styles for badges, cards, loading states
|
||||
- Validation: Styles applied consistently
|
||||
|
||||
- [x] 046: Update index.html
|
||||
- Location: `NEW/src/JdeScoping.Client/wwwroot/index.html`
|
||||
- Add: Radzen CSS reference, JS interop script reference
|
||||
- Validation: Radzen styles load correctly
|
||||
|
||||
## Phase 14: Verification
|
||||
|
||||
- [x] 047: Verify all pages render
|
||||
- Navigate to each route, verify content loads
|
||||
- Validation: No console errors, all pages accessible
|
||||
|
||||
- [x] 048: Verify form validation
|
||||
- Test Login form, Search Create form with invalid data
|
||||
- Validation: Validation messages display correctly
|
||||
|
||||
- [x] 049: Verify SignalR connection
|
||||
- Check browser console for connection logs
|
||||
- Validation: Connection established, reconnects on disconnect
|
||||
|
||||
- [x] 050: Run solution build
|
||||
- Command: `dotnet build NEW/JdeScoping.sln`
|
||||
- Validation: No errors or warnings
|
||||
|
||||
- [x] 051: Run OpenSpec validation
|
||||
- Command: `openspec validate implement-blazor-ui --strict`
|
||||
- Validation: No validation errors
|
||||
Reference in New Issue
Block a user