Files
jdescopingtool/PLANS/2026-01-06-blazor-migration-implementation.md
T
Joseph Doherty d4135e8ad3 fix(data-access): correct self-referential SQL in WorkCenter filter
The WHERE clause was comparing Code to itself instead of the aliased
table reference, which would always be true.
2026-01-06 14:12:07 -05:00

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