Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,89 @@
using System.Net.Http.Json;
using JdeScoping.Client.Auth;
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Handles authentication via API calls with cookie-based auth.
/// </summary>
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthStateProvider _authStateProvider;
public AuthService(
HttpClient httpClient,
AuthStateProvider authStateProvider)
{
_httpClient = httpClient;
_authStateProvider = authStateProvider;
}
public async Task<AuthResult> LoginAsync(LoginModel model)
{
try
{
var response = await _httpClient.PostAsJsonAsync("api/auth/login", new
{
model.Username,
model.Password
});
if (response.IsSuccessStatusCode)
{
// API returns UserInfo and sets auth cookie
var userInfo = await response.Content.ReadFromJsonAsync<UserInfoViewModel>();
if (userInfo != null)
{
// Notify auth state provider of the login
await _authStateProvider.MarkUserAsAuthenticated(userInfo);
return new AuthResult
{
Success = true,
User = userInfo
};
}
return new AuthResult
{
Success = false,
ErrorMessage = "Invalid response from server"
};
}
var errorContent = await response.Content.ReadAsStringAsync();
return new AuthResult
{
Success = false,
ErrorMessage = string.IsNullOrEmpty(errorContent)
? "Login failed. Please check your credentials."
: errorContent
};
}
catch (Exception ex)
{
return new AuthResult
{
Success = false,
ErrorMessage = $"Login failed: {ex.Message}"
};
}
}
public async Task LogoutAsync()
{
try
{
// Call logout endpoint to clear server-side cookie
await _httpClient.PostAsync("api/auth/logout", null);
}
catch
{
// Even if logout API fails, clear local state
}
await _authStateProvider.LogoutAsync();
}
}
@@ -0,0 +1,115 @@
using System.Net.Http.Json;
using JdeScoping.Client.Models;
using JdeScoping.Core.ViewModels;
using Microsoft.JSInterop;
namespace JdeScoping.Client.Services;
/// <summary>
/// Handles file upload/download operations via the api/fileio endpoints.
/// Authentication is handled via cookies (sent automatically by the browser).
/// </summary>
public class FileService : IFileService
{
private readonly HttpClient _httpClient;
private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient httpClient, IJSRuntime jsRuntime)
{
_httpClient = httpClient;
_jsRuntime = jsRuntime;
}
public async Task DownloadTemplateAsync(string templateType, object? existingData = null)
{
try
{
// Map template type to API endpoint
var endpoint = templateType switch
{
"work-orders" or "workorders" => "api/fileio/workorders/download",
"items" or "part-numbers" => "api/fileio/items/download",
"component-lots" or "componentlots" => "api/fileio/componentlots/download",
"part-operations" or "partoperations" => "api/fileio/partoperations/download",
_ => throw new ArgumentException($"Unknown template type: {templateType}")
};
var fileName = templateType switch
{
"work-orders" or "workorders" => "work_order_template.xlsx",
"items" or "part-numbers" => "item_number_template.xlsx",
"component-lots" or "componentlots" => "component_lot_template.xlsx",
"part-operations" or "partoperations" => "item_operations_mis_template.xlsx",
_ => $"{templateType}_template.xlsx"
};
// POST with existing data to get the Excel file
var response = await _httpClient.PostAsJsonAsync(endpoint, existingData);
if (response.IsSuccessStatusCode)
{
var bytes = await response.Content.ReadAsByteArrayAsync();
await _jsRuntime.InvokeVoidAsync("downloadFile", fileName, bytes);
}
else
{
Console.WriteLine($"Failed to download template: {response.StatusCode}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to download template: {ex.Message}");
}
}
public async Task DownloadPartNumberTemplateAsync(List<ItemViewModel>? existingItems = null)
{
await DownloadTemplateAsync("items", existingItems);
}
public async Task<UploadResult<T>> UploadAsync<T>(string uploadType, Stream fileStream, string fileName)
{
try
{
// Map upload type to API endpoint
var endpoint = uploadType switch
{
"work-orders" or "workorders" => "api/fileio/workorders/upload",
"items" or "part-numbers" => "api/fileio/items/upload",
"component-lots" or "componentlots" => "api/fileio/componentlots/upload",
"part-operations" or "partoperations" => "api/fileio/partoperations/upload",
_ => throw new ArgumentException($"Unknown upload type: {uploadType}")
};
using var content = new MultipartFormDataContent();
using var streamContent = new StreamContent(fileStream);
content.Add(streamContent, "file", fileName);
var response = await _httpClient.PostAsync(endpoint, content);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<UploadResult<T>>();
return result ?? new UploadResult<T>
{
WasSuccessful = false,
ErrorMessage = "Invalid response from server"
};
}
return new UploadResult<T>
{
WasSuccessful = false,
ErrorMessage = $"Upload failed: {response.StatusCode}"
};
}
catch (Exception ex)
{
return new UploadResult<T>
{
WasSuccessful = false,
ErrorMessage = $"Upload failed: {ex.Message}"
};
}
}
}
@@ -0,0 +1,117 @@
using JdeScoping.Client.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
namespace JdeScoping.Client.Services;
/// <summary>
/// Manages SignalR connection with auto-reconnect.
/// Uses cookie-based authentication (browser automatically sends cookies with requests).
/// </summary>
public class HubConnectionService : IHubConnectionService, IAsyncDisposable
{
private readonly NavigationManager _navigationManager;
private HubConnection? _hubConnection;
public event Action<SearchUpdate>? OnSearchUpdate;
public event Action<StatusUpdate>? OnStatusUpdate;
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
public HubConnectionService(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}
public async Task StartAsync()
{
if (_hubConnection != null)
{
return;
}
// In Blazor WebAssembly, the browser automatically sends cookies with requests
// to the same origin, so we don't need to configure any special auth options
_hubConnection = new HubConnectionBuilder()
.WithUrl(_navigationManager.ToAbsoluteUri("/hubs/status"))
.WithAutomaticReconnect([
TimeSpan.Zero,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
])
.Build();
_hubConnection.On<SearchUpdate>("searchUpdate", update =>
{
OnSearchUpdate?.Invoke(update);
});
_hubConnection.On<StatusUpdate>("statusUpdate", update =>
{
OnStatusUpdate?.Invoke(update);
});
_hubConnection.Reconnecting += error =>
{
Console.WriteLine($"SignalR reconnecting: {error?.Message}");
return Task.CompletedTask;
};
_hubConnection.Reconnected += connectionId =>
{
Console.WriteLine($"SignalR reconnected: {connectionId}");
return Task.CompletedTask;
};
_hubConnection.Closed += error =>
{
Console.WriteLine($"SignalR closed: {error?.Message}");
return Task.CompletedTask;
};
try
{
await _hubConnection.StartAsync();
Console.WriteLine("SignalR connected");
}
catch (Exception ex)
{
Console.WriteLine($"SignalR connection failed: {ex.Message}");
}
}
public async Task StopAsync()
{
if (_hubConnection != null)
{
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
_hubConnection = null;
}
}
public async Task<StatusUpdate?> GetCachedStatusAsync()
{
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
{
return null;
}
try
{
return await _hubConnection.InvokeAsync<StatusUpdate>("GetCachedStatus");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get cached status: {ex.Message}");
return null;
}
}
public async ValueTask DisposeAsync()
{
await StopAsync();
}
}
@@ -0,0 +1,29 @@
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for authentication operations.
/// </summary>
public interface IAuthService
{
/// <summary>
/// Attempts to log in with the provided credentials.
/// </summary>
Task<AuthResult> LoginAsync(LoginModel model);
/// <summary>
/// Logs out the current user.
/// </summary>
Task LogoutAsync();
}
/// <summary>
/// Result of an authentication attempt.
/// </summary>
public record AuthResult
{
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
public UserInfoViewModel? User { get; init; }
}
@@ -0,0 +1,35 @@
using JdeScoping.Client.Models;
using JdeScoping.Core.ViewModels;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for file upload/download operations.
/// </summary>
public interface IFileService
{
/// <summary>
/// Downloads the work order template file.
/// </summary>
Task DownloadTemplateAsync(string templateType, object? existingData = null);
/// <summary>
/// Downloads the part number template file.
/// </summary>
Task DownloadPartNumberTemplateAsync(List<ItemViewModel>? existingItems = null);
/// <summary>
/// Uploads a file and returns parsed data.
/// </summary>
Task<UploadResult<T>> UploadAsync<T>(string uploadType, Stream fileStream, string fileName);
}
/// <summary>
/// Result of a file upload operation.
/// </summary>
public class UploadResult<T>
{
public bool WasSuccessful { get; set; }
public string? ErrorMessage { get; set; }
public List<T> Data { get; set; } = [];
}
@@ -0,0 +1,39 @@
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for managing SignalR hub connection.
/// </summary>
public interface IHubConnectionService
{
/// <summary>
/// Event fired when a search update is received.
/// </summary>
event Action<SearchUpdate>? OnSearchUpdate;
/// <summary>
/// Event fired when a processor status update is received.
/// </summary>
event Action<StatusUpdate>? OnStatusUpdate;
/// <summary>
/// Starts the SignalR connection.
/// </summary>
Task StartAsync();
/// <summary>
/// Stops the SignalR connection.
/// </summary>
Task StopAsync();
/// <summary>
/// Gets the cached processor status from the server.
/// </summary>
Task<StatusUpdate?> GetCachedStatusAsync();
/// <summary>
/// Gets the current connection state.
/// </summary>
bool IsConnected { get; }
}
@@ -0,0 +1,30 @@
using JdeScoping.Client.Models;
using JdeScoping.Core.ViewModels;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for lookup/autocomplete API operations.
/// </summary>
public interface ILookupService
{
/// <summary>
/// Finds items matching the search term.
/// </summary>
Task<List<ItemViewModel>> FindItemsAsync(string searchTerm);
/// <summary>
/// Finds profit centers matching the search term.
/// </summary>
Task<List<ProfitCenterViewModel>> FindProfitCentersAsync(string searchTerm);
/// <summary>
/// Finds work centers matching the search term.
/// </summary>
Task<List<WorkCenterViewModel>> FindWorkCentersAsync(string searchTerm);
/// <summary>
/// Finds operators matching the search term.
/// </summary>
Task<List<OperatorViewModel>> FindOperatorsAsync(string searchTerm);
}
@@ -0,0 +1,14 @@
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for data refresh status API operations.
/// </summary>
public interface IRefreshStatusService
{
/// <summary>
/// Gets refresh status records within the specified date range.
/// </summary>
Task<List<DataUpdateViewModel>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt);
}
@@ -0,0 +1,39 @@
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Service for search-related API operations.
/// </summary>
public interface ISearchService
{
/// <summary>
/// Gets all searches for the current user.
/// </summary>
Task<List<SearchViewModel>> GetUserSearchesAsync();
/// <summary>
/// Gets a specific search by ID.
/// </summary>
Task<SearchViewModel?> GetSearchAsync(int id);
/// <summary>
/// Copies an existing search to create a new one.
/// </summary>
Task<SearchViewModel?> CopySearchAsync(int id);
/// <summary>
/// Saves and submits a search.
/// </summary>
Task<int?> SaveSearchAsync(SearchViewModel search);
/// <summary>
/// Gets all searches in the queue.
/// </summary>
Task<List<SearchViewModel>> GetQueueAsync();
/// <summary>
/// Downloads the results for a completed search.
/// </summary>
Task<byte[]?> DownloadResultsAsync(int id);
}
@@ -0,0 +1,99 @@
using System.Net.Http.Json;
using JdeScoping.Client.Models;
using JdeScoping.Core.ViewModels;
namespace JdeScoping.Client.Services;
/// <summary>
/// Handles lookup/autocomplete API calls.
/// Authentication is handled via cookies (sent automatically by the browser).
/// </summary>
public class LookupService : ILookupService
{
private readonly HttpClient _httpClient;
public LookupService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<ItemViewModel>> FindItemsAsync(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm) || searchTerm.Length < 3)
{
return [];
}
try
{
var result = await _httpClient.GetFromJsonAsync<List<ItemViewModel>>(
$"api/lookup/items?q={Uri.EscapeDataString(searchTerm)}");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to find items: {ex.Message}");
return [];
}
}
public async Task<List<ProfitCenterViewModel>> FindProfitCentersAsync(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm) || searchTerm.Length < 3)
{
return [];
}
try
{
var result = await _httpClient.GetFromJsonAsync<List<ProfitCenterViewModel>>(
$"api/lookup/profit-centers?q={Uri.EscapeDataString(searchTerm)}");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to find profit centers: {ex.Message}");
return [];
}
}
public async Task<List<WorkCenterViewModel>> FindWorkCentersAsync(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm) || searchTerm.Length < 3)
{
return [];
}
try
{
var result = await _httpClient.GetFromJsonAsync<List<WorkCenterViewModel>>(
$"api/lookup/work-centers?q={Uri.EscapeDataString(searchTerm)}");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to find work centers: {ex.Message}");
return [];
}
}
public async Task<List<OperatorViewModel>> FindOperatorsAsync(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm) || searchTerm.Length < 3)
{
return [];
}
try
{
var result = await _httpClient.GetFromJsonAsync<List<OperatorViewModel>>(
$"api/lookup/operators?q={Uri.EscapeDataString(searchTerm)}");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to find operators: {ex.Message}");
return [];
}
}
}
@@ -0,0 +1,35 @@
using System.Net.Http.Json;
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Handles refresh status API calls.
/// Authentication is handled via cookies (sent automatically by the browser).
/// </summary>
public class RefreshStatusService : IRefreshStatusService
{
private readonly HttpClient _httpClient;
public RefreshStatusService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<DataUpdateViewModel>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt)
{
try
{
var minDtStr = minDt.ToString("yyyy-MM-dd");
var maxDtStr = maxDt.ToString("yyyy-MM-dd");
var result = await _httpClient.GetFromJsonAsync<List<DataUpdateViewModel>>(
$"api/refresh-status?minDT={minDtStr}&maxDT={maxDtStr}");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get refresh status: {ex.Message}");
return [];
}
}
}
@@ -0,0 +1,129 @@
using System.Net.Http.Json;
using JdeScoping.Client.Models;
namespace JdeScoping.Client.Services;
/// <summary>
/// Handles search-related API calls.
/// Authentication is handled via cookies (sent automatically by the browser).
/// </summary>
public class SearchService : ISearchService
{
private readonly HttpClient _httpClient;
public SearchService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<SearchViewModel>> GetUserSearchesAsync()
{
try
{
var result = await _httpClient.GetFromJsonAsync<List<SearchViewModel>>("api/search");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get searches: {ex.Message}");
return [];
}
}
public async Task<SearchViewModel?> GetSearchAsync(int id)
{
try
{
return await _httpClient.GetFromJsonAsync<SearchViewModel>($"api/search/{id}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get search {id}: {ex.Message}");
return null;
}
}
public async Task<SearchViewModel?> CopySearchAsync(int id)
{
try
{
return await _httpClient.GetFromJsonAsync<SearchViewModel>($"api/search/{id}/copy");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to copy search {id}: {ex.Message}");
return null;
}
}
public async Task<int?> SaveSearchAsync(SearchViewModel search)
{
try
{
var response = await _httpClient.PostAsJsonAsync("api/search", new
{
search.Name,
search.UserName,
Criteria = new
{
MinimumDT = search.Criteria.MinimumDt,
MaximumDT = search.Criteria.MaximumDt,
WorkOrders = search.Criteria.WorkOrders.Select(wo => new { wo.WorkOrderNumber }),
Items = search.Criteria.Items.Select(i => new { i.ItemNumber }),
ProfitCenters = search.Criteria.ProfitCenters.Select(pc => new { pc.Code }),
WorkCenters = search.Criteria.WorkCenters.Select(wc => new { wc.Code }),
ComponentLots = search.Criteria.ComponentLots.Select(cl => new { cl.LotNumber, cl.ItemNumber }),
Operators = search.Criteria.Operators.Select(op => new { op.AddressNumber, UserID = op.UserId }),
PartOperations = search.Criteria.PartOperations.Select(po => new
{
po.ItemNumber,
po.OperationNumber,
po.MisNumber,
po.MisRevision
}),
search.Criteria.ExtractMisData
}
});
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<int>();
}
Console.WriteLine($"Failed to save search: {response.StatusCode}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save search: {ex.Message}");
return null;
}
}
public async Task<List<SearchViewModel>> GetQueueAsync()
{
try
{
var result = await _httpClient.GetFromJsonAsync<List<SearchViewModel>>("api/search/queue");
return result ?? [];
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get queue: {ex.Message}");
return [];
}
}
public async Task<byte[]?> DownloadResultsAsync(int id)
{
try
{
return await _httpClient.GetByteArrayAsync($"api/search/{id}/results");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to download results for {id}: {ex.Message}");
return null;
}
}
}