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

31 KiB

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

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

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

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

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:

// 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:

using JdeScoping.Client.Http;

Step 3: Verify build

Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj Expected: Build succeeded

Step 4: Commit

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:

@inject ISearchService SearchService

To:

@inject ISearchApiClient SearchApi

Step 2: Add using for extensions

Add after @page directives:

@using JdeScoping.Client.Extensions

Step 3: Update LoadSearchesAsync method

Replace the entire method with:

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:

private string? _errorMessage;

Add error display in markup after loading indicator:

@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

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:

@inject ISearchService SearchService

To:

@inject ISearchApiClient SearchApi

Step 2: Add using for extensions

Add after @page directive:

@using JdeScoping.Client.Extensions

Step 3: Update LoadQueueAsync method

Replace with:

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:

private string? _errorMessage;

Add error display after loading indicator.

Step 5: Verify build and commit

Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj

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:

@inject ISearchService SearchService
@inject IFileService FileService

To:

@inject ISearchApiClient SearchApi
@inject IFileApiClient FileApi

Step 2: Add using for extensions

@using JdeScoping.Client.Extensions

Step 3: Update LoadSearchAsync method

Replace entire method with:

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

private string? _errorMessage;

Step 5: Verify build

Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj Expected: Build succeeded

Step 6: Commit

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:

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:

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

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:

@inject IAuthService AuthService

To:

@inject IAuthApiClient AuthApi
@inject ICryptoService CryptoService

Step 2: Update HandleLoginAsync method

Replace with:

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

@using JdeScoping.Core.Models

Step 4: Verify build and commit

Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj

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:

@inject IAuthService AuthService

To:

@inject IAuthApiClient AuthApi

Step 2: Update LogoutAsync method

Replace with:

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

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:

@inject ILookupService LookupService
@inject IFileService FileService

To:

@inject ILookupApiClient LookupApi
@inject IFileApiClient FileApi

Step 2: Update OnSearchAsync method

Replace with:

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:

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:

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

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

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

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

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

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

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

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:

builder.Services.AddScoped<ISearchService, SearchService>();
builder.Services.AddScoped<ILookupService, LookupService>();
builder.Services.AddScoped<IFileService, FileService>();

Keep:

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

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

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

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:

dotnet clean
dotnet build

Expected: Build succeeded with 0 errors

Step 2: Run any available tests

Run:

dotnet test

Expected: All tests pass

Step 3: Commit if any fixes needed

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:

@using JdeScoping.Client.Extensions

Step 2: Verify build and commit

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