docs: add XML documentation and ConfigManager implementation plans

Add comprehensive XML documentation (param/returns tags) across 132 source
files to improve IntelliSense and API discoverability. Include ConfigManager
design documents and implementation plans for phases 1-9.
This commit is contained in:
Joseph Doherty
2026-01-20 02:26:26 -05:00
parent c044337539
commit d49330e697
136 changed files with 9181 additions and 4 deletions
@@ -18,6 +18,12 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
private readonly ILogger<AuthStateProvider>? _logger;
private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity());
/// <summary>
/// Initializes a new instance of the AuthStateProvider class.
/// </summary>
/// <param name="userStorage">Service for storing user authentication data.</param>
/// <param name="httpClient">The HTTP client for making API requests.</param>
/// <param name="logger">Optional logger for debugging authentication issues.</param>
public AuthStateProvider(
IUserStorageService userStorage,
HttpClient httpClient,
@@ -28,6 +34,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
_logger = logger;
}
/// <inheritdoc />
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// First check cached user info
@@ -93,6 +100,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
/// <summary>
/// Called after successful login to update auth state.
/// </summary>
/// <param name="user">The authenticated user information.</param>
public async Task MarkUserAsAuthenticated(UserInfoDto user)
{
await _userStorage.SetUserAsync(user);
@@ -17,6 +17,7 @@ public interface IUserStorageService
/// <summary>
/// Stores the user info.
/// </summary>
/// <param name="user">The user information to store.</param>
Task SetUserAsync(UserInfoDto user);
/// <summary>
@@ -14,11 +14,19 @@ public class UserStorageService : IUserStorageService
private const string UserKey = "jdescoping_user";
private readonly IJSRuntime _jsRuntime;
/// <summary>
/// Initializes a new instance of the <see cref="UserStorageService"/> class.
/// </summary>
/// <param name="jsRuntime">The JS interop runtime for accessing browser storage.</param>
public UserStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
/// <summary>
/// Retrieves the stored user information from browser session storage.
/// </summary>
/// <returns>A task that completes with the user information, or null if not found.</returns>
public async Task<UserInfoDto?> GetUserAsync()
{
try
@@ -40,12 +48,21 @@ public class UserStorageService : IUserStorageService
}
}
/// <summary>
/// Stores user information in browser session storage.
/// </summary>
/// <param name="user">The user information to store.</param>
/// <returns>A task that completes when the user information is stored.</returns>
public async Task SetUserAsync(UserInfoDto user)
{
var json = JsonSerializer.Serialize(user);
await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.setSessionStorage", UserKey, json);
}
/// <summary>
/// Removes the stored user information from browser session storage.
/// </summary>
/// <returns>A task that completes when the user information is removed.</returns>
public async Task RemoveUserAsync()
{
await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.removeSessionStorage", UserKey);
@@ -9,6 +9,9 @@ namespace JdeScoping.Client.Components.FilterPanels;
/// <typeparam name="TItem">The type of items displayed in the grid.</typeparam>
public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where TItem : class
{
/// <summary>
/// Gets or sets the Radzen dialog service for confirming user actions.
/// </summary>
[Inject]
protected DialogService DialogService { get; set; } = default!;
@@ -73,21 +76,29 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Performs the search API call and returns matching items.
/// </summary>
/// <param name="filter">The search filter text.</param>
/// <returns>A task that returns the list of matching items.</returns>
protected abstract Task<List<TItem>> SearchApiAsync(string filter);
/// <summary>
/// Gets the unique key value for an item.
/// </summary>
/// <param name="item">The item to get the key for.</param>
/// <returns>The unique key value.</returns>
protected abstract object GetItemKey(TItem item);
/// <summary>
/// Gets the display text value for an item (used for matching in autocomplete).
/// </summary>
/// <param name="item">The item to get the display text for.</param>
/// <returns>The display text value.</returns>
protected abstract string GetDisplayText(TItem item);
/// <summary>
/// Handles the autocomplete search.
/// </summary>
/// <param name="args">The load data arguments containing the search filter.</param>
/// <returns>A task representing the asynchronous search operation.</returns>
protected async Task OnSearchAsync(LoadDataArgs args)
{
if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3)
@@ -103,6 +114,7 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Handles selection from the autocomplete.
/// </summary>
/// <param name="value">The selected value from the autocomplete control.</param>
protected void OnItemSelected(object value)
{
if (value is string text && !string.IsNullOrEmpty(text))
@@ -138,6 +150,8 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Removes an item from the list.
/// </summary>
/// <param name="item">The item to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
protected async Task DeleteItem(TItem item)
{
Items.Remove(item);
@@ -11,12 +11,15 @@ namespace JdeScoping.Client.Components.FilterPanels;
/// <typeparam name="TItem">The type of items displayed in the grid.</typeparam>
public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TItem : class
{
/// <summary>Service for managing dialogs.</summary>
[Inject]
protected DialogService DialogService { get; set; } = default!;
/// <summary>Service for displaying notifications.</summary>
[Inject]
protected NotificationService NotificationService { get; set; } = default!;
/// <summary>JavaScript interop runtime.</summary>
[Inject]
protected IJSRuntime JSRuntime { get; set; } = default!;
@@ -81,6 +84,8 @@ public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TIt
/// <summary>
/// Uploads the file and returns the parsed items.
/// </summary>
/// <param name="stream">The file stream to upload.</param>
/// <param name="filename">The name of the file being uploaded.</param>
protected abstract Task<List<TItem>?> UploadFileApiAsync(Stream stream, string filename);
/// <summary>
@@ -106,6 +111,7 @@ public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TIt
/// <summary>
/// Handles file selection and upload.
/// </summary>
/// <param name="e">The file change event arguments.</param>
protected async Task OnFileSelected(InputFileChangeEventArgs e)
{
if (e.File == null) return;
@@ -22,6 +22,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Core SearchViewModel to Client SearchViewModel.
/// </summary>
/// <param name="vm">The Core SearchViewModel to map.</param>
public static SearchViewModel ToClient(this CoreSearch vm) => new()
{
Id = vm.Id,
@@ -37,6 +38,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Client SearchViewModel to Core SearchViewModel.
/// </summary>
/// <param name="vm">The Client SearchViewModel to map.</param>
public static CoreSearch ToCore(this SearchViewModel vm)
{
ArgumentNullException.ThrowIfNull(vm);
@@ -58,6 +60,7 @@ public static class ViewModelMappingExtensions
/// Maps Core SearchCriteria to Client SearchCriteriaViewModel.
/// Core uses primitive lists; Client uses full view model objects.
/// </summary>
/// <param name="criteria">The Core SearchCriteria to map.</param>
public static SearchCriteriaViewModel ToClientCriteria(this SearchCriteria criteria)
{
var client = new SearchCriteriaViewModel
@@ -107,6 +110,7 @@ public static class ViewModelMappingExtensions
/// Maps Client SearchCriteriaViewModel to Core SearchCriteria.
/// Client uses full view model objects; Core uses primitive lists.
/// </summary>
/// <param name="criteria">The Client SearchCriteriaViewModel to map.</param>
public static SearchCriteria ToCoreCriteria(this SearchCriteriaViewModel criteria)
{
ArgumentNullException.ThrowIfNull(criteria);
@@ -131,6 +135,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Core JdeUserViewModel to Client OperatorViewModel.
/// </summary>
/// <param name="vm">The Core JdeUserViewModel to map.</param>
public static OperatorViewModel ToClientOperator(this CoreJdeUser vm) => new()
{
AddressNumber = vm.AddressNumber,
@@ -141,6 +146,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps a collection of Core SearchViewModels to Client SearchViewModels.
/// </summary>
/// <param name="list">The collection of Core SearchViewModels to map.</param>
public static List<SearchViewModel> ToClientList(this IEnumerable<CoreSearch> list)
{
ArgumentNullException.ThrowIfNull(list);
@@ -150,6 +156,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps a collection of Core JdeUserViewModels to Client OperatorViewModels.
/// </summary>
/// <param name="list">The collection of Core JdeUserViewModels to map.</param>
public static List<OperatorViewModel> ToClientOperatorList(this IEnumerable<CoreJdeUser> list)
{
ArgumentNullException.ThrowIfNull(list);
@@ -11,11 +11,16 @@ public class AuthRedirectHandler : DelegatingHandler
{
private readonly NavigationManager _navigationManager;
/// <summary>
/// Initializes a new instance of the AuthRedirectHandler class.
/// </summary>
/// <param name="navigationManager">The navigation manager for redirecting to login.</param>
public AuthRedirectHandler(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
@@ -7,16 +7,53 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class SearchCriteriaViewModel
{
/// <summary>
/// Gets or sets the minimum date for search filtering.
/// </summary>
public DateTime? MinimumDt { get; set; }
/// <summary>
/// Gets or sets the maximum date for search filtering.
/// </summary>
public DateTime? MaximumDt { get; set; }
/// <summary>
/// Gets or sets the list of work orders to include in the search.
/// </summary>
public List<WorkOrderViewModel> WorkOrders { get; set; } = [];
/// <summary>
/// Gets or sets the list of items to include in the search.
/// </summary>
public List<ItemViewModel> Items { get; set; } = [];
/// <summary>
/// Gets or sets the list of profit centers to include in the search.
/// </summary>
public List<ProfitCenterViewModel> ProfitCenters { get; set; } = [];
/// <summary>
/// Gets or sets the list of work centers to include in the search.
/// </summary>
public List<WorkCenterViewModel> WorkCenters { get; set; } = [];
/// <summary>
/// Gets or sets the list of component lots to include in the search.
/// </summary>
public List<ComponentLotViewModel> ComponentLots { get; set; } = [];
/// <summary>
/// Gets or sets the list of operators to include in the search.
/// </summary>
public List<OperatorViewModel> Operators { get; set; } = [];
/// <summary>
/// Gets or sets the list of part operations to include in the search.
/// </summary>
public List<PartOperationViewModel> PartOperations { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether to extract MIS data in the search.
/// </summary>
public bool ExtractMisData { get; set; }
}
@@ -7,15 +7,45 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class SearchViewModel
{
/// <summary>
/// Gets or sets the unique identifier for the search.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the search.
/// </summary>
[Required(ErrorMessage = "Name is required.")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the username of the user who created the search.
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current status of the search.
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the date and time when the search was submitted.
/// </summary>
public DateTime? SubmitDt { get; set; }
/// <summary>
/// Gets or sets the date and time when the search execution started.
/// </summary>
public DateTime? StartDt { get; set; }
/// <summary>
/// Gets or sets the date and time when the search execution ended.
/// </summary>
public DateTime? EndDt { get; set; }
/// <summary>
/// Gets or sets the search criteria for filtering.
/// </summary>
public SearchCriteriaViewModel Criteria { get; set; } = new();
/// <summary>
@@ -6,21 +6,74 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class ValidCombination
{
/// <summary>
/// The unique identifier for this combination.
/// </summary>
public int Id { get; private init; }
/// <summary>
/// The display name for this combination.
/// </summary>
public string Name { get; private init; } = string.Empty;
/// <summary>
/// Whether the timespan filter is included in this combination.
/// </summary>
public bool Timespan { get; private init; }
/// <summary>
/// Whether the work order filter is included in this combination.
/// </summary>
public bool WorkOrder { get; private init; }
/// <summary>
/// Whether the item number filter is included in this combination.
/// </summary>
public bool ItemNumber { get; private init; }
/// <summary>
/// Whether the profit center filter is included in this combination.
/// </summary>
public bool ProfitCenter { get; private init; }
/// <summary>
/// Whether the work center filter is included in this combination.
/// </summary>
public bool WorkCenter { get; private init; }
/// <summary>
/// Whether the component lot filter is included in this combination.
/// </summary>
public bool ComponentLot { get; private init; }
/// <summary>
/// Whether the operator filter is included in this combination.
/// </summary>
public bool Operator { get; private init; }
/// <summary>
/// Whether the item operation MIS filter is included in this combination.
/// </summary>
public bool ItemOperationMis { get; private init; }
/// <summary>
/// Whether the extract MIS filter is included in this combination.
/// </summary>
public bool ExtractMis { get; private init; }
/// <summary>
/// Checks if the given filter flags match this combination.
/// </summary>
/// <param name="timespan">Whether timespan filter is enabled.</param>
/// <param name="workOrder">Whether work order filter is enabled.</param>
/// <param name="itemNumber">Whether item number filter is enabled.</param>
/// <param name="profitCenter">Whether profit center filter is enabled.</param>
/// <param name="workCenter">Whether work center filter is enabled.</param>
/// <param name="componentLot">Whether component lot filter is enabled.</param>
/// <param name="operator">Whether operator filter is enabled.</param>
/// <param name="itemOperationMis">Whether item operation MIS filter is enabled.</param>
/// <param name="extractMis">Whether extract MIS filter is enabled.</param>
/// <returns>True if all flags match this combination.</returns>
public bool Matches(
bool timespan,
bool workOrder,
@@ -17,26 +17,55 @@ public abstract class ApiClientBase
PropertyNameCaseInsensitive = true
};
/// <summary>
/// Initializes a new instance of the <see cref="ApiClientBase"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client to use for requests.</param>
protected ApiClientBase(HttpClient httpClient)
{
HttpClient = httpClient;
}
/// <summary>
/// Executes a GET request and deserializes the response to the specified type.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> GetAsync<T>(string route, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.GetAsync(route, ct));
}
/// <summary>
/// Executes a POST request with a JSON body.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <typeparam name="TBody">The request body type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="body">The request body.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostAsync<T, TBody>(string route, TBody body, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.PostAsJsonAsync(route, body, ct));
}
/// <summary>
/// Executes a POST request without a body.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostAsync<T>(string route, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.PostAsync(route, null, ct));
}
/// <summary>
/// Executes a GET request and returns the response as raw bytes.
/// </summary>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<byte[]>> GetBytesAsync(string route, CancellationToken ct = default)
{
try
@@ -50,6 +79,13 @@ public abstract class ApiClientBase
}
}
/// <summary>
/// Executes a POST request with a JSON body and returns the response as raw bytes.
/// </summary>
/// <typeparam name="TBody">The request body type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="body">The request body.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<byte[]>> PostForBytesAsync<TBody>(string route, TBody body, CancellationToken ct = default)
{
try
@@ -63,6 +99,14 @@ public abstract class ApiClientBase
}
}
/// <summary>
/// Executes a multipart POST request with a file stream.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The file name for the multipart form.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostMultipartAsync<T>(
string route,
Stream fileStream,
@@ -161,6 +205,9 @@ public abstract class ApiClientBase
/// </summary>
private sealed class ValidationProblemDetails
{
/// <summary>
/// Gets or sets the validation errors by field name.
/// </summary>
public Dictionary<string, string[]>? Errors { get; set; }
}
}
@@ -9,17 +9,42 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class AuthApiClient : ApiClientBase, IAuthApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public AuthApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Retrieves the server's RSA public key for credential encryption.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The public key response.</returns>
public Task<ApiResult<PublicKeyResponse>> GetPublicKeyAsync(CancellationToken ct = default)
=> GetAsync<PublicKeyResponse>(ApiRoutes.Auth.PublicKey, ct);
/// <summary>
/// Authenticates a user with encrypted credentials.
/// </summary>
/// <param name="request">The encrypted login request.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The login result with user info on success.</returns>
public Task<ApiResult<LoginResultModel>> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default)
=> PostAsync<LoginResultModel, EncryptedLoginRequest>(ApiRoutes.Auth.Login, request, ct);
/// <summary>
/// Logs out the current user.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Result of logout operation.</returns>
public Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default)
=> PostAsync<Unit>(ApiRoutes.Auth.Logout, ct);
/// <summary>
/// Retrieves information about the current authenticated user.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The current user's information.</returns>
public Task<ApiResult<UserInfoDto>> GetCurrentUserAsync(CancellationToken ct = default)
=> GetAsync<UserInfoDto>(ApiRoutes.Auth.Me, ct);
}
@@ -13,6 +13,12 @@ public class AuthService : IAuthService
private readonly ICryptoService _cryptoService;
private readonly IAuthStateProvider _authStateProvider;
/// <summary>
/// Initializes a new instance of the <see cref="AuthService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
/// <param name="cryptoService">The cryptography service.</param>
/// <param name="authStateProvider">The authentication state provider.</param>
public AuthService(
HttpClient httpClient,
ICryptoService cryptoService,
@@ -23,6 +29,11 @@ public class AuthService : IAuthService
_authStateProvider = authStateProvider;
}
/// <summary>
/// Authenticates a user with encrypted credentials.
/// </summary>
/// <param name="model">The login credentials.</param>
/// <returns>The login result with user information if successful.</returns>
public async Task<LoginResultModel> LoginAsync(LoginModel model)
{
try
@@ -54,6 +65,9 @@ public class AuthService : IAuthService
}
}
/// <summary>
/// Logs out the current user and clears authentication state.
/// </summary>
public async Task LogoutAsync()
{
try
@@ -17,6 +17,11 @@ public class CryptoService : ICryptoService, IAsyncDisposable
private readonly SemaphoreSlim _keyLock = new(1, 1);
private bool _disposed;
/// <summary>
/// Initializes a new instance of the CryptoService.
/// </summary>
/// <param name="httpClient">The HTTP client for fetching the public key from the server.</param>
/// <param name="jsRuntime">The JavaScript runtime for performing RSA encryption.</param>
public CryptoService(HttpClient httpClient, IJSRuntime jsRuntime)
{
_httpClient = httpClient;
@@ -9,33 +9,89 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class FileApiClient : ApiClientBase, IFileApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="FileApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public FileApiClient(HttpClient httpClient) : base(httpClient) { }
// Downloads
/// <summary>
/// Downloads an Excel template with optional existing work order data.
/// </summary>
/// <param name="existingData">Optional list of existing work orders to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadWorkOrdersTemplateAsync(IReadOnlyList<WorkOrderViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadWorkOrders, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing item data.
/// </summary>
/// <param name="existingData">Optional list of existing items to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadItemsTemplateAsync(IReadOnlyList<ItemViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadItems, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing component lot data.
/// </summary>
/// <param name="existingData">Optional list of existing lots to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadComponentLotsTemplateAsync(IReadOnlyList<LotViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadComponentLots, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing part operation data.
/// </summary>
/// <param name="existingData">Optional list of existing part operations to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadPartOperationsTemplateAsync(IReadOnlyList<PartOperationViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadPartOperations, existingData, ct);
// Uploads
/// <summary>
/// Uploads and parses an Excel file containing work orders.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed work order view models.</returns>
public Task<ApiResult<IReadOnlyList<WorkOrderViewModel>>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<WorkOrderViewModel>>(ApiRoutes.FileIO.UploadWorkOrders, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing items.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed item view models.</returns>
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.FileIO.UploadItems, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing component lots.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed lot view models.</returns>
public Task<ApiResult<IReadOnlyList<LotViewModel>>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<LotViewModel>>(ApiRoutes.FileIO.UploadComponentLots, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing part operations.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed part operation view models.</returns>
public Task<ApiResult<IReadOnlyList<PartOperationViewModel>>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<PartOperationViewModel>>(ApiRoutes.FileIO.UploadPartOperations, fileStream, fileName, ct);
}
@@ -13,16 +13,33 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
private readonly NavigationManager _navigationManager;
private HubConnection? _hubConnection;
/// <summary>
/// Raised when a search is updated.
/// </summary>
public event Action<SearchUpdateViewModel>? OnSearchUpdate;
/// <summary>
/// Raised when a status update is received.
/// </summary>
public event Action<StatusUpdateViewModel>? OnStatusUpdate;
/// <summary>
/// Gets a value indicating whether the SignalR connection is active.
/// </summary>
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
/// <summary>
/// Initializes a new instance of the <see cref="HubConnectionService"/> class.
/// </summary>
/// <param name="navigationManager">The navigation manager for resolving hub URL.</param>
public HubConnectionService(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}
/// <summary>
/// Establishes the SignalR connection and registers message handlers.
/// </summary>
public async Task StartAsync()
{
if (_hubConnection != null)
@@ -82,6 +99,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Stops the SignalR connection.
/// </summary>
public async Task StopAsync()
{
if (_hubConnection != null)
@@ -92,6 +112,10 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Retrieves the most recent status update from the server cache.
/// </summary>
/// <returns>The cached status update, or null if not available.</returns>
public async Task<StatusUpdateViewModel?> GetCachedStatusAsync()
{
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
@@ -110,6 +134,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Releases the resources used by the service.
/// </summary>
public async ValueTask DisposeAsync()
{
await StopAsync();
@@ -10,6 +10,8 @@ public interface IAuthService
/// <summary>
/// Attempts to log in with the provided credentials (encrypted).
/// </summary>
/// <param name="model">The login credentials.</param>
/// <returns>The login result containing status and user information.</returns>
Task<LoginResultModel> LoginAsync(LoginModel model);
/// <summary>
@@ -10,5 +10,8 @@ public interface IRefreshStatusService
/// <summary>
/// Gets refresh status records within the specified date range.
/// </summary>
/// <param name="minDt">The minimum date/time (inclusive).</param>
/// <param name="maxDt">The maximum date/time (inclusive).</param>
/// <returns>A list of data update status records.</returns>
Task<List<DataUpdateDto>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt);
}
@@ -38,10 +38,14 @@ public class SearchSubmissionResult
/// <summary>
/// Creates a successful result.
/// </summary>
/// <param name="searchId">The ID of the successfully submitted search.</param>
/// <returns>A successful search submission result.</returns>
public static SearchSubmissionResult Success(int searchId) => new() { SearchId = searchId };
/// <summary>
/// Creates a failure result.
/// </summary>
/// <param name="error">The error message describing the submission failure.</param>
/// <returns>A failed search submission result.</returns>
public static SearchSubmissionResult Failure(string error) => new() { ErrorMessage = error };
}
@@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class LookupApiClient : ApiClientBase, ILookupApiClient
{
/// <summary>
/// Initializes a new instance of the LookupApiClient class.
/// </summary>
/// <param name="httpClient">The HTTP client for making API requests.</param>
public LookupApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Finds items matching the search query.
/// </summary>
/// <param name="query">The search query to match against item data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching items.</returns>
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> FindItemsAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.Lookup.FindItems(query), ct);
/// <summary>
/// Finds profit centers matching the search query.
/// </summary>
/// <param name="query">The search query to match against profit center data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching profit centers.</returns>
public Task<ApiResult<IReadOnlyList<ProfitCenterViewModel>>> FindProfitCentersAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<ProfitCenterViewModel>>(ApiRoutes.Lookup.FindProfitCenters(query), ct);
/// <summary>
/// Finds work centers matching the search query.
/// </summary>
/// <param name="query">The search query to match against work center data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching work centers.</returns>
public Task<ApiResult<IReadOnlyList<WorkCenterViewModel>>> FindWorkCentersAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<WorkCenterViewModel>>(ApiRoutes.Lookup.FindWorkCenters(query), ct);
/// <summary>
/// Finds operators (JDE users) matching the search query.
/// </summary>
/// <param name="query">The search query to match against operator data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching operators.</returns>
public Task<ApiResult<IReadOnlyList<JdeUserViewModel>>> FindOperatorsAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<JdeUserViewModel>>(ApiRoutes.Lookup.FindOperators(query), ct);
}
@@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class PipelineApiClient : ApiClientBase, IPipelineApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="PipelineApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Gets the list of available pipeline names from the API.
/// </summary>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline list.</returns>
public Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default)
=> GetAsync<PipelineListResponse>(ApiRoutes.Pipelines.Base, ct);
/// <summary>
/// Gets the configuration for a specific pipeline by name.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline configuration.</returns>
public Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineConfigDto>(ApiRoutes.Pipelines.GetByName(name), ct);
/// <summary>
/// Gets the current execution status of a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline status.</returns>
public Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineStatusResponse>(ApiRoutes.Pipelines.GetStatus(name), ct);
/// <summary>
/// Gets the recent execution history for a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="count">The maximum number of execution records to retrieve.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the execution history.</returns>
public Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
=> GetAsync<PipelineExecutionsResponse>(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
}
@@ -11,11 +11,21 @@ public class RefreshStatusService : IRefreshStatusService
{
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshStatusService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
public RefreshStatusService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Gets the data refresh status for a date range.
/// </summary>
/// <param name="minDt">The minimum date for the status query.</param>
/// <param name="maxDt">The maximum date for the status query.</param>
/// <returns>A list of data update information for the specified date range.</returns>
public async Task<List<DataUpdateDto>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt)
{
try
@@ -9,23 +9,61 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class SearchApiClient : ApiClientBase, ISearchApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="SearchApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public SearchApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Retrieves the current user's searches.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of user searches.</returns>
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetUserSearchesAsync(CancellationToken ct = default)
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Base, ct);
/// <summary>
/// Retrieves searches queued for processing.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of queued searches.</returns>
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetQueuedSearchesAsync(CancellationToken ct = default)
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Queue, ct);
/// <summary>
/// Retrieves a specific search by ID.
/// </summary>
/// <param name="id">The search ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The search view model.</returns>
public Task<ApiResult<SearchViewModel>> GetSearchAsync(int id, CancellationToken ct = default)
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetById(id), ct);
/// <summary>
/// Creates a copy of an existing search.
/// </summary>
/// <param name="id">The ID of the search to copy.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The new copied search view model.</returns>
public Task<ApiResult<SearchViewModel>> CopySearchAsync(int id, CancellationToken ct = default)
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetCopy(id), ct);
/// <summary>
/// Creates a new search.
/// </summary>
/// <param name="search">The search view model to create.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The ID of the newly created search.</returns>
public Task<ApiResult<int>> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default)
=> PostAsync<int, SearchViewModel>(ApiRoutes.Search.Base, search, ct);
/// <summary>
/// Retrieves the results of a completed search.
/// </summary>
/// <param name="id">The search ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes containing results.</returns>
public Task<ApiResult<byte[]>> GetResultsAsync(int id, CancellationToken ct = default)
=> GetBytesAsync(ApiRoutes.Search.GetResults(id), ct);
}
@@ -11,6 +11,10 @@ public class SearchSubmissionService : ISearchSubmissionService
{
private readonly ISearchApiClient _searchApi;
/// <summary>
/// Initializes a new instance of the <see cref="SearchSubmissionService"/> class.
/// </summary>
/// <param name="searchApi">The search API client.</param>
public SearchSubmissionService(ISearchApiClient searchApi)
{
_searchApi = searchApi;