d4135e8ad3
The WHERE clause was comparing Code to itself instead of the aliased table reference, which would always be true.
1108 lines
31 KiB
Markdown
1108 lines
31 KiB
Markdown
# Blazor Component Migration Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Migrate all Blazor components from old `I*Service` interfaces to new `I*ApiClient` interfaces with proper `ApiResult<T>` error handling.
|
|
|
|
**Architecture:** Create AuthRedirectHandler for global 401 handling, view model mapping extensions for Core<->Client conversion, then update each component to inject API clients and use `result.Switch()` pattern.
|
|
|
|
**Tech Stack:** Blazor WebAssembly, OneOf discriminated unions, DelegatingHandler, extension methods
|
|
|
|
---
|
|
|
|
### Task 1: Create AuthRedirectHandler
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.Client/Http/AuthRedirectHandler.cs`
|
|
|
|
**Step 1: Create the handler file**
|
|
|
|
```csharp
|
|
using System.Net;
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace JdeScoping.Client.Http;
|
|
|
|
/// <summary>
|
|
/// HTTP message handler that intercepts 401 Unauthorized responses
|
|
/// and redirects to the login page with return URL.
|
|
/// </summary>
|
|
public class AuthRedirectHandler : DelegatingHandler
|
|
{
|
|
private readonly NavigationManager _navigationManager;
|
|
|
|
public AuthRedirectHandler(NavigationManager navigationManager)
|
|
{
|
|
_navigationManager = navigationManager;
|
|
}
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var response = await base.SendAsync(request, cancellationToken);
|
|
|
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
{
|
|
var returnUrl = Uri.EscapeDataString(_navigationManager.Uri);
|
|
_navigationManager.NavigateTo($"/login?returnUrl={returnUrl}", forceLoad: true);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify file compiles**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Http/AuthRedirectHandler.cs
|
|
git commit -m "feat(client): add AuthRedirectHandler for global 401 redirect"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Create View Model Mapping Extensions
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs`
|
|
|
|
**Step 1: Create the mapping extensions file**
|
|
|
|
```csharp
|
|
using JdeScoping.Client.Models;
|
|
using JdeScoping.Core.Models.Enums;
|
|
using JdeScoping.Core.Models.Search;
|
|
using CoreSearch = JdeScoping.Core.ViewModels.SearchViewModel;
|
|
using CoreItem = JdeScoping.Core.ViewModels.ItemViewModel;
|
|
using CoreWorkOrder = JdeScoping.Core.ViewModels.WorkOrderViewModel;
|
|
using CoreProfitCenter = JdeScoping.Core.ViewModels.ProfitCenterViewModel;
|
|
using CoreWorkCenter = JdeScoping.Core.ViewModels.WorkCenterViewModel;
|
|
using CoreLot = JdeScoping.Core.ViewModels.LotViewModel;
|
|
using CorePartOp = JdeScoping.Core.ViewModels.PartOperationViewModel;
|
|
using CoreJdeUser = JdeScoping.Core.ViewModels.JdeUserViewModel;
|
|
|
|
namespace JdeScoping.Client.Extensions;
|
|
|
|
/// <summary>
|
|
/// Extension methods for mapping between Core and Client view models.
|
|
/// </summary>
|
|
public static class ViewModelMappingExtensions
|
|
{
|
|
// SearchViewModel: Core -> Client
|
|
public static SearchViewModel ToClient(this CoreSearch vm) => new()
|
|
{
|
|
Id = vm.Id,
|
|
Name = vm.Name,
|
|
UserName = vm.UserName,
|
|
Status = vm.Status.ToString(),
|
|
SubmitDt = vm.SubmitDt,
|
|
StartDt = vm.StartDt,
|
|
EndDt = vm.EndDt,
|
|
Criteria = vm.Criteria?.ToClientCriteria() ?? new()
|
|
};
|
|
|
|
// SearchViewModel: Client -> Core
|
|
public static CoreSearch ToCore(this SearchViewModel vm) => new()
|
|
{
|
|
Id = vm.Id,
|
|
Name = vm.Name,
|
|
UserName = vm.UserName,
|
|
Status = Enum.TryParse<SearchStatus>(vm.Status, out var status) ? status : SearchStatus.New,
|
|
SubmitDt = vm.SubmitDt,
|
|
StartDt = vm.StartDt,
|
|
EndDt = vm.EndDt,
|
|
Criteria = vm.Criteria.ToCoreCriteria()
|
|
};
|
|
|
|
// SearchCriteria: Core -> Client
|
|
public static SearchCriteriaViewModel ToClientCriteria(this SearchCriteria criteria)
|
|
{
|
|
var client = new SearchCriteriaViewModel
|
|
{
|
|
MinimumDt = criteria.MinimumDt,
|
|
MaximumDt = criteria.MaximumDt,
|
|
ExtractMisData = criteria.ExtractMisData
|
|
};
|
|
|
|
// Map work orders (Core has just numbers, Client has full objects)
|
|
client.WorkOrders = criteria.WorkOrderNumbers
|
|
.Select(n => new CoreWorkOrder { WorkOrderNumber = n })
|
|
.ToList();
|
|
|
|
// Map items
|
|
client.Items = criteria.ItemNumbers
|
|
.Select(n => new CoreItem { ItemNumber = n })
|
|
.ToList();
|
|
|
|
// Map profit centers
|
|
client.ProfitCenters = criteria.ProfitCenters
|
|
.Select(pc => new CoreProfitCenter { ProfitCenterCode = pc })
|
|
.ToList();
|
|
|
|
// Map work centers
|
|
client.WorkCenters = criteria.WorkCenters
|
|
.Select(wc => new CoreWorkCenter { WorkCenterCode = wc })
|
|
.ToList();
|
|
|
|
// Map operators
|
|
client.Operators = criteria.OperatorIDs
|
|
.Select(id => new OperatorViewModel { UserId = id })
|
|
.ToList();
|
|
|
|
// Map component lots (Core and Client both use LotViewModel)
|
|
client.ComponentLots = criteria.ComponentLotNumbers
|
|
.Select(l => new ComponentLotViewModel { LotNumber = l.LotNumber, ItemNumber = l.ItemNumber })
|
|
.ToList();
|
|
|
|
// Map part operations (same structure)
|
|
client.PartOperations = criteria.PartOperations.ToList();
|
|
|
|
return client;
|
|
}
|
|
|
|
// SearchCriteria: Client -> Core
|
|
public static SearchCriteria ToCoreCriteria(this SearchCriteriaViewModel criteria) => new()
|
|
{
|
|
MinimumDt = criteria.MinimumDt,
|
|
MaximumDt = criteria.MaximumDt,
|
|
ExtractMisData = criteria.ExtractMisData,
|
|
WorkOrderNumbers = criteria.WorkOrders.Select(wo => wo.WorkOrderNumber).ToList(),
|
|
ItemNumbers = criteria.Items.Select(i => i.ItemNumber).ToList(),
|
|
ProfitCenters = criteria.ProfitCenters.Select(pc => pc.ProfitCenterCode).ToList(),
|
|
WorkCenters = criteria.WorkCenters.Select(wc => wc.WorkCenterCode).ToList(),
|
|
OperatorIDs = criteria.Operators.Select(o => o.UserId).ToList(),
|
|
ComponentLotNumbers = criteria.ComponentLots
|
|
.Select(l => new CoreLot { LotNumber = l.LotNumber, ItemNumber = l.ItemNumber })
|
|
.ToList(),
|
|
PartOperations = criteria.PartOperations.ToList()
|
|
};
|
|
|
|
// JdeUserViewModel -> OperatorViewModel
|
|
public static OperatorViewModel ToClientOperator(this CoreJdeUser vm) => new()
|
|
{
|
|
AddressNumber = (int)vm.AddressNumber,
|
|
UserId = vm.UserId,
|
|
FullName = vm.FullName
|
|
};
|
|
|
|
// Collection helpers
|
|
public static List<SearchViewModel> ToClientList(this IEnumerable<CoreSearch> list) =>
|
|
list.Select(s => s.ToClient()).ToList();
|
|
|
|
public static List<OperatorViewModel> ToClientOperatorList(this IEnumerable<CoreJdeUser> list) =>
|
|
list.Select(u => u.ToClientOperator()).ToList();
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify file compiles**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs
|
|
git commit -m "feat(client): add ViewModelMappingExtensions for Core<->Client mapping"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Update Program.cs with HttpClient Handler Pipeline
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Program.cs`
|
|
|
|
**Step 1: Update HttpClient registration to use handler**
|
|
|
|
Replace the current HttpClient registration with handler pipeline:
|
|
|
|
```csharp
|
|
// OLD:
|
|
builder.Services.AddScoped(sp => new HttpClient
|
|
{
|
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
|
});
|
|
|
|
// NEW:
|
|
builder.Services.AddScoped<AuthRedirectHandler>();
|
|
builder.Services.AddScoped(sp =>
|
|
{
|
|
var navigationManager = sp.GetRequiredService<NavigationManager>();
|
|
var handler = new AuthRedirectHandler(navigationManager)
|
|
{
|
|
InnerHandler = new HttpClientHandler()
|
|
};
|
|
return new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
|
};
|
|
});
|
|
```
|
|
|
|
**Step 2: Add required using statement**
|
|
|
|
Add at top of file:
|
|
```csharp
|
|
using JdeScoping.Client.Http;
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Program.cs
|
|
git commit -m "feat(client): configure HttpClient with AuthRedirectHandler"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Migrate Searches.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Pages/Searches.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change:
|
|
```razor
|
|
@inject ISearchService SearchService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject ISearchApiClient SearchApi
|
|
```
|
|
|
|
**Step 2: Add using for extensions**
|
|
|
|
Add after @page directives:
|
|
```razor
|
|
@using JdeScoping.Client.Extensions
|
|
```
|
|
|
|
**Step 3: Update LoadSearchesAsync method**
|
|
|
|
Replace the entire method with:
|
|
|
|
```csharp
|
|
private async Task LoadSearchesAsync()
|
|
{
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
try
|
|
{
|
|
var result = await SearchApi.GetUserSearchesAsync();
|
|
result.Switch(
|
|
searches => { _searches = searches.ToClientList(); },
|
|
notFound => { _errorMessage = "No searches found."; _searches = []; },
|
|
validation => { _errorMessage = validation.Message; },
|
|
unauthorized => { _errorMessage = "Session expired. Please login again."; },
|
|
forbidden => { _errorMessage = "Access denied."; },
|
|
error => { _errorMessage = error.Message; }
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 4: Add error message field and display**
|
|
|
|
Add field in @code section:
|
|
```csharp
|
|
private string? _errorMessage;
|
|
```
|
|
|
|
Add error display in markup after loading indicator:
|
|
```razor
|
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
|
{
|
|
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true" Variant="Variant.Flat" class="rz-mb-4">
|
|
@_errorMessage
|
|
</RadzenAlert>
|
|
}
|
|
```
|
|
|
|
**Step 5: Verify build**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Pages/Searches.razor
|
|
git commit -m "feat(client): migrate Searches.razor to ISearchApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Migrate SearchQueue.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Pages/SearchQueue.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change:
|
|
```razor
|
|
@inject ISearchService SearchService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject ISearchApiClient SearchApi
|
|
```
|
|
|
|
**Step 2: Add using for extensions**
|
|
|
|
Add after @page directive:
|
|
```razor
|
|
@using JdeScoping.Client.Extensions
|
|
```
|
|
|
|
**Step 3: Update LoadQueueAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task LoadQueueAsync()
|
|
{
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
try
|
|
{
|
|
var result = await SearchApi.GetQueuedSearchesAsync();
|
|
result.Switch(
|
|
searches => { _searches = searches.ToClientList(); },
|
|
notFound => { _errorMessage = "Queue not found."; _searches = []; },
|
|
validation => { _errorMessage = validation.Message; },
|
|
unauthorized => { _errorMessage = "Session expired. Please login again."; },
|
|
forbidden => { _errorMessage = "Access denied."; },
|
|
error => { _errorMessage = error.Message; }
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 4: Add error message field and display**
|
|
|
|
Add field:
|
|
```csharp
|
|
private string? _errorMessage;
|
|
```
|
|
|
|
Add error display after loading indicator.
|
|
|
|
**Step 5: Verify build and commit**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Pages/SearchQueue.razor
|
|
git commit -m "feat(client): migrate SearchQueue.razor to ISearchApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Migrate SearchEdit.razor - Part 1 (Load Methods)
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Pages/SearchEdit.razor`
|
|
|
|
**Step 1: Update inject statements**
|
|
|
|
Change:
|
|
```razor
|
|
@inject ISearchService SearchService
|
|
@inject IFileService FileService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject ISearchApiClient SearchApi
|
|
@inject IFileApiClient FileApi
|
|
```
|
|
|
|
**Step 2: Add using for extensions**
|
|
|
|
```razor
|
|
@using JdeScoping.Client.Extensions
|
|
```
|
|
|
|
**Step 3: Update LoadSearchAsync method**
|
|
|
|
Replace entire method with:
|
|
|
|
```csharp
|
|
private async Task LoadSearchAsync()
|
|
{
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
try
|
|
{
|
|
if (CopySearchId.HasValue)
|
|
{
|
|
var result = await SearchApi.CopySearchAsync(CopySearchId.Value);
|
|
result.Switch(
|
|
copied => {
|
|
_search = copied.ToClient();
|
|
_search.Id = 0;
|
|
_search.Status = "New";
|
|
},
|
|
notFound => { _errorMessage = "Search to copy not found."; },
|
|
validation => { _errorMessage = validation.Message; },
|
|
unauthorized => { _errorMessage = "Session expired."; },
|
|
forbidden => { _errorMessage = "Access denied."; },
|
|
error => { _errorMessage = error.Message; }
|
|
);
|
|
}
|
|
else if (Id.HasValue && Id.Value > 0)
|
|
{
|
|
var result = await SearchApi.GetSearchAsync(Id.Value);
|
|
result.Switch(
|
|
loaded => { _search = loaded.ToClient(); },
|
|
notFound => { _errorMessage = "Search not found."; },
|
|
validation => { _errorMessage = validation.Message; },
|
|
unauthorized => { _errorMessage = "Session expired."; },
|
|
forbidden => { _errorMessage = "Access denied."; },
|
|
error => { _errorMessage = error.Message; }
|
|
);
|
|
}
|
|
else
|
|
{
|
|
_search = new ClientSearchViewModel
|
|
{
|
|
Status = "New",
|
|
UserName = await AuthStateProvider.GetUsernameAsync() ?? "",
|
|
Criteria = new SearchCriteriaViewModel()
|
|
};
|
|
}
|
|
|
|
DetectSearchType();
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 4: Add error message field**
|
|
|
|
```csharp
|
|
private string? _errorMessage;
|
|
```
|
|
|
|
**Step 5: Verify build**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Pages/SearchEdit.razor
|
|
git commit -m "feat(client): migrate SearchEdit.razor load methods to API clients"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Migrate SearchEdit.razor - Part 2 (Save and Download Methods)
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Pages/SearchEdit.razor`
|
|
|
|
**Step 1: Update SubmitSearchInternalAsync method**
|
|
|
|
Replace the save call:
|
|
|
|
```csharp
|
|
private async Task SubmitSearchInternalAsync()
|
|
{
|
|
var confirmed = await DialogService.Confirm("Are you sure you want to submit the search?", "Confirm Submit", new ConfirmOptions
|
|
{
|
|
OkButtonText = "Submit",
|
|
CancelButtonText = "Cancel"
|
|
});
|
|
|
|
if (confirmed != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isSubmitting = true;
|
|
try
|
|
{
|
|
var result = await SearchApi.CreateSearchAsync(_search.ToCore());
|
|
result.Switch(
|
|
id => { NavigationManager.NavigateTo($"/search/{id}"); },
|
|
notFound => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Search not found."); },
|
|
validation => { NotificationService.Notify(NotificationSeverity.Error, "Validation Error", validation.Message); },
|
|
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
|
|
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
|
|
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
_isSubmitting = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Update DownloadResultsAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task DownloadResultsAsync()
|
|
{
|
|
var result = await SearchApi.GetResultsAsync(_search.Id);
|
|
result.Switch(
|
|
bytes =>
|
|
{
|
|
if (bytes.Length > 0)
|
|
{
|
|
_ = JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", bytes);
|
|
NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully.");
|
|
}
|
|
else
|
|
{
|
|
NotificationService.Notify(NotificationSeverity.Warning, "Download", "No results available to download.");
|
|
}
|
|
},
|
|
notFound => { NotificationService.Notify(NotificationSeverity.Warning, "Download", "Results not found."); },
|
|
validation => { NotificationService.Notify(NotificationSeverity.Error, "Error", validation.Message); },
|
|
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
|
|
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
|
|
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
|
|
);
|
|
}
|
|
```
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Pages/SearchEdit.razor
|
|
git commit -m "feat(client): migrate SearchEdit.razor save and download methods"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Migrate Login.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Pages/Login.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change:
|
|
```razor
|
|
@inject IAuthService AuthService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject IAuthApiClient AuthApi
|
|
@inject ICryptoService CryptoService
|
|
```
|
|
|
|
**Step 2: Update HandleLoginAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task HandleLoginAsync()
|
|
{
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
|
|
try
|
|
{
|
|
// Encrypt credentials
|
|
var encryptedData = await CryptoService.EncryptLoginAsync(_loginModel);
|
|
var request = new EncryptedLoginRequest(encryptedData);
|
|
|
|
var result = await AuthApi.LoginAsync(request);
|
|
result.Switch(
|
|
loginResult =>
|
|
{
|
|
if (loginResult.Success && loginResult.User is not null)
|
|
{
|
|
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl;
|
|
NavigationManager.NavigateTo(returnUrl);
|
|
}
|
|
else
|
|
{
|
|
_errorMessage = loginResult.ErrorMessage ?? "Login failed. Please check your credentials.";
|
|
}
|
|
},
|
|
notFound => { _errorMessage = "Authentication service not found."; },
|
|
validation => { _errorMessage = validation.Message; },
|
|
unauthorized => { _errorMessage = "Invalid credentials."; },
|
|
forbidden => { _errorMessage = "Access denied."; },
|
|
error => { _errorMessage = error.Message; }
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = $"An error occurred: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Add required using**
|
|
|
|
```razor
|
|
@using JdeScoping.Core.Models
|
|
```
|
|
|
|
**Step 4: Verify build and commit**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Pages/Login.razor
|
|
git commit -m "feat(client): migrate Login.razor to IAuthApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Migrate MainLayout.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Layout/MainLayout.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change:
|
|
```razor
|
|
@inject IAuthService AuthService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject IAuthApiClient AuthApi
|
|
```
|
|
|
|
**Step 2: Update LogoutAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task LogoutAsync()
|
|
{
|
|
await AuthApi.LogoutAsync();
|
|
NavigationManager.NavigateTo("/login");
|
|
}
|
|
```
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Layout/MainLayout.razor
|
|
git commit -m "feat(client): migrate MainLayout.razor to IAuthApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Migrate ItemNumberFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statements**
|
|
|
|
Change:
|
|
```razor
|
|
@inject ILookupService LookupService
|
|
@inject IFileService FileService
|
|
```
|
|
To:
|
|
```razor
|
|
@inject ILookupApiClient LookupApi
|
|
@inject IFileApiClient FileApi
|
|
```
|
|
|
|
**Step 2: Update OnSearchAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task OnSearchAsync(LoadDataArgs args)
|
|
{
|
|
if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3)
|
|
{
|
|
var result = await LookupApi.FindItemsAsync(args.Filter);
|
|
result.Switch(
|
|
items => { _searchResults = items.ToList(); },
|
|
_ => { _searchResults = []; },
|
|
_ => { _searchResults = []; },
|
|
_ => { _searchResults = []; },
|
|
_ => { _searchResults = []; },
|
|
_ => { _searchResults = []; }
|
|
);
|
|
}
|
|
else
|
|
{
|
|
_searchResults = [];
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Update DownloadTemplateAsync method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task DownloadTemplateAsync()
|
|
{
|
|
var result = await FileApi.DownloadItemsTemplateAsync(Items.AsReadOnly());
|
|
result.Switch(
|
|
bytes => { _ = JSRuntime.InvokeVoidAsync("downloadFile", "items_template.xlsx", bytes); },
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Template not found."); },
|
|
validation => { NotificationService.Notify(NotificationSeverity.Error, "Error", validation.Message); },
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
|
|
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
|
|
);
|
|
}
|
|
```
|
|
|
|
**Step 4: Update OnFileSelected method**
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
private async Task OnFileSelected(InputFileChangeEventArgs e)
|
|
{
|
|
if (e.File == null) return;
|
|
|
|
_isUploading = true;
|
|
try
|
|
{
|
|
using var stream = e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
|
|
var result = await FileApi.UploadItemsAsync(stream, e.File.Name);
|
|
|
|
result.Switch(
|
|
items =>
|
|
{
|
|
Items.Clear();
|
|
Items.AddRange(items);
|
|
_ = ItemsChanged.InvokeAsync(Items);
|
|
NotificationService.Notify(NotificationSeverity.Success, "Upload Complete", $"Loaded {items.Count} items.");
|
|
},
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Upload endpoint not found."); },
|
|
validation => { NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", validation.Message); },
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
|
|
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
|
|
error => { NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", error.Message); }
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
_isUploading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 5: Verify build and commit**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
|
|
git commit -m "feat(client): migrate ItemNumberFilterPanel to API clients"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Migrate WorkCenterFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change `@inject ILookupService LookupService` to `@inject ILookupApiClient LookupApi`
|
|
|
|
**Step 2: Update search method to use ApiResult pattern**
|
|
|
|
Similar to ItemNumberFilterPanel, update the autocomplete search to use `result.Switch()`.
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
|
|
git commit -m "feat(client): migrate WorkCenterFilterPanel to ILookupApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Migrate ProfitCenterFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change `@inject ILookupService LookupService` to `@inject ILookupApiClient LookupApi`
|
|
|
|
**Step 2: Update search method to use ApiResult pattern**
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
|
|
git commit -m "feat(client): migrate ProfitCenterFilterPanel to ILookupApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 13: Migrate OperatorFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement and add using**
|
|
|
|
Change `@inject ILookupService LookupService` to `@inject ILookupApiClient LookupApi`
|
|
|
|
Add: `@using JdeScoping.Client.Extensions`
|
|
|
|
**Step 2: Update search method**
|
|
|
|
Use `result.Switch()` and map JdeUserViewModel to OperatorViewModel using `.ToClientOperatorList()`.
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
|
|
git commit -m "feat(client): migrate OperatorFilterPanel to ILookupApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Migrate WorkOrderFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/WorkOrderFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change `@inject IFileService FileService` to `@inject IFileApiClient FileApi`
|
|
|
|
**Step 2: Update download and upload methods**
|
|
|
|
Use `result.Switch()` pattern for `DownloadWorkOrdersTemplateAsync` and `UploadWorkOrdersAsync`.
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/WorkOrderFilterPanel.razor
|
|
git commit -m "feat(client): migrate WorkOrderFilterPanel to IFileApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 15: Migrate ComponentLotFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/ComponentLotFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change `@inject IFileService FileService` to `@inject IFileApiClient FileApi`
|
|
|
|
**Step 2: Update download and upload methods**
|
|
|
|
Use `result.Switch()` pattern for `DownloadComponentLotsTemplateAsync` and `UploadComponentLotsAsync`.
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/ComponentLotFilterPanel.razor
|
|
git commit -m "feat(client): migrate ComponentLotFilterPanel to IFileApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 16: Migrate PartOperationFilterPanel.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Components/FilterPanels/PartOperationFilterPanel.razor`
|
|
|
|
**Step 1: Update inject statement**
|
|
|
|
Change `@inject IFileService FileService` to `@inject IFileApiClient FileApi`
|
|
|
|
**Step 2: Update download and upload methods**
|
|
|
|
Use `result.Switch()` pattern for `DownloadPartOperationsTemplateAsync` and `UploadPartOperationsAsync`.
|
|
|
|
**Step 3: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Components/FilterPanels/PartOperationFilterPanel.razor
|
|
git commit -m "feat(client): migrate PartOperationFilterPanel to IFileApiClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 17: Update Program.cs - Remove Old Service Registrations
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/Program.cs`
|
|
|
|
**Step 1: Remove old service registrations**
|
|
|
|
Remove these lines:
|
|
```csharp
|
|
builder.Services.AddScoped<ISearchService, SearchService>();
|
|
builder.Services.AddScoped<ILookupService, LookupService>();
|
|
builder.Services.AddScoped<IFileService, FileService>();
|
|
```
|
|
|
|
Keep:
|
|
```csharp
|
|
builder.Services.AddScoped<IAuthService, AuthService>(); // Keep temporarily - AuthService has crypto logic
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded with no references to old services
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/Program.cs
|
|
git commit -m "chore(client): remove old service registrations"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 18: Delete Old Service Files
|
|
|
|
**Files:**
|
|
- Delete: `src/JdeScoping.Client/Services/ISearchService.cs`
|
|
- Delete: `src/JdeScoping.Client/Services/SearchService.cs`
|
|
- Delete: `src/JdeScoping.Client/Services/ILookupService.cs`
|
|
- Delete: `src/JdeScoping.Client/Services/LookupService.cs`
|
|
- Delete: `src/JdeScoping.Client/Services/IFileService.cs`
|
|
- Delete: `src/JdeScoping.Client/Services/FileService.cs`
|
|
|
|
**Step 1: Delete files**
|
|
|
|
```bash
|
|
rm src/JdeScoping.Client/Services/ISearchService.cs
|
|
rm src/JdeScoping.Client/Services/SearchService.cs
|
|
rm src/JdeScoping.Client/Services/ILookupService.cs
|
|
rm src/JdeScoping.Client/Services/LookupService.cs
|
|
rm src/JdeScoping.Client/Services/IFileService.cs
|
|
rm src/JdeScoping.Client/Services/FileService.cs
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore(client): delete old service files replaced by API clients"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 19: Final Build Verification
|
|
|
|
**Files:**
|
|
- All modified files
|
|
|
|
**Step 1: Clean and rebuild entire solution**
|
|
|
|
Run:
|
|
```bash
|
|
dotnet clean
|
|
dotnet build
|
|
```
|
|
Expected: Build succeeded with 0 errors
|
|
|
|
**Step 2: Run any available tests**
|
|
|
|
Run:
|
|
```bash
|
|
dotnet test
|
|
```
|
|
Expected: All tests pass
|
|
|
|
**Step 3: Commit if any fixes needed**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix(client): address any build issues from migration"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 20: Update _Imports.razor
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Client/_Imports.razor`
|
|
|
|
**Step 1: Add using for extensions namespace**
|
|
|
|
Add:
|
|
```razor
|
|
@using JdeScoping.Client.Extensions
|
|
```
|
|
|
|
**Step 2: Verify build and commit**
|
|
|
|
```bash
|
|
git add src/JdeScoping.Client/_Imports.razor
|
|
git commit -m "chore(client): add Extensions namespace to global imports"
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
This plan migrates 12 Blazor components from old `I*Service` interfaces to new `I*ApiClient` interfaces:
|
|
- 5 pages (Searches, SearchQueue, SearchEdit, Login, MainLayout)
|
|
- 7 filter panels (ItemNumber, WorkCenter, ProfitCenter, Operator, WorkOrder, ComponentLot, PartOperation)
|
|
|
|
Key changes:
|
|
1. Global 401 handling via AuthRedirectHandler
|
|
2. View model mapping via extension methods
|
|
3. ApiResult<T> pattern with Switch() for error handling
|
|
4. Delete 6 old service files after migration
|