# 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` 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; /// /// HTTP message handler that intercepts 401 Unauthorized responses /// and redirects to the login page with return URL. /// public class AuthRedirectHandler : DelegatingHandler { private readonly NavigationManager _navigationManager; public AuthRedirectHandler(NavigationManager navigationManager) { _navigationManager = navigationManager; } protected override async Task 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; /// /// Extension methods for mapping between Core and Client view models. /// 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(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 ToClientList(this IEnumerable list) => list.Select(s => s.ToClient()).ToList(); public static List ToClientOperatorList(this IEnumerable 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(); builder.Services.AddScoped(sp => { var navigationManager = sp.GetRequiredService(); 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)) { @_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/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(); builder.Services.AddScoped(); builder.Services.AddScoped(); ``` Keep: ```csharp builder.Services.AddScoped(); // 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 pattern with Switch() for error handling 4. Delete 6 old service files after migration