Files
jdescopingtool/NEW/src/JdeScoping.Client/Pages/SearchEdit.razor
T
2026-02-13 10:11:01 -05:00

313 lines
12 KiB
Plaintext

@*
SearchEdit.razor - Main search creation and editing page.
Handles creating new searches, editing existing drafts, and viewing completed searches.
Integrates with SignalR for real-time status updates during search execution.
*@
@page "/search"
@page "/search/{Id:int}"
@attribute [Authorize]
@using JdeScoping.Core.ApiContracts
@using JdeScoping.Client.Extensions
@using JdeScoping.Client.Auth
@using JdeScoping.Client.Components.Search
@using Microsoft.JSInterop
@inject ISearchApiClient SearchApi
@inject IHubConnectionService HubConnection
@inject IAuthStateProvider AuthStateProvider
@inject ISearchValidationService ValidationService
@inject ISearchSubmissionService SubmissionService
@inject NavigationManager NavigationManager
@inject DialogService DialogService
@inject NotificationService NotificationService
@inject IJSRuntime JSRuntime
@implements IDisposable
<PageTitle>@(_search.Id == 0 ? "New Search" : "Search") - JDE Scoping Tool</PageTitle>
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.SpaceBetween" class="rz-mb-4">
<RadzenText TextStyle="TextStyle.H4" class="rz-m-0">Search</RadzenText>
@if (!_search.IsReadOnly)
{
<RadzenButton Text="Submit" Icon="send" ButtonStyle="ButtonStyle.Primary" Click="@SubmitSearchAsync" IsBusy="@_isSubmitting" BusyText="Submitting..." class="btn-submit-blue" />
}
</RadzenStack>
@if (_isLoading)
{
<LoadingIndicator Message="Loading search..." />
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true" Variant="Variant.Flat" class="rz-mb-4">
@_errorMessage
</RadzenAlert>
}
else
{
<SignalRStatusHandler SearchId="@_search.Id" OnStatusChanged="HandleSearchUpdate" />
<FilterVisibilityManager @ref="_visibilityManager" Criteria="@_search.Criteria" OnSearchTypeChanged="OnSearchTypeChanged">
<EditForm Model="@_search" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
@if (_search.IsReadOnly)
{
<RadzenAlert AlertStyle="AlertStyle.Warning" ShowIcon="true" Variant="Variant.Flat" class="rz-mb-4">
<strong>Note:</strong> Search is read-only because it has already been submitted. To change or re-run the search again click the Copy button.
<RadzenButton Text="Copy" ButtonStyle="ButtonStyle.Secondary" Size="ButtonSize.Small" Click="@CopySearchAsync" class="rz-ml-2" />
</RadzenAlert>
}
<ValidationSummary class="rz-mb-4" />
<SearchDetailsSection
Search="@_search"
@bind-SelectedSearchType="@_visibilityManager.SelectedSearchType"
ValidCombinations="@_visibilityManager.ValidCombinations"
OnDownloadResults="@DownloadResultsAsync" />
@if (_visibilityManager.ShowTimespan)
{
<TimeSpanFilterPanel @bind-MinimumDT="_search.Criteria.MinimumDt" @bind-MaximumDT="_search.Criteria.MaximumDt" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowWorkOrder)
{
<WorkOrderFilterPanel @bind-WorkOrders="_search.Criteria.WorkOrders" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowItemNumber)
{
<ItemNumberFilterPanel @bind-Items="_search.Criteria.Items" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowProfitCenter)
{
<ProfitCenterFilterPanel @bind-ProfitCenters="_search.Criteria.ProfitCenters" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowWorkCenter)
{
<WorkCenterFilterPanel @bind-WorkCenters="_search.Criteria.WorkCenters" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowComponentLot)
{
<ComponentLotFilterPanel @bind-ComponentLots="_search.Criteria.ComponentLots" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowOperator)
{
<OperatorFilterPanel @bind-Operators="_search.Criteria.Operators" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowItemOperationMis)
{
<PartOperationFilterPanel @bind-PartOperations="_search.Criteria.PartOperations" IsReadOnly="@_search.IsReadOnly" />
}
@if (_visibilityManager.ShowExtractMis)
{
<RadzenCard class="rz-mb-4">
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
<RadzenCheckBox @bind-Value="_search.Criteria.ExtractMisData" Disabled="true" />
<RadzenText TextStyle="TextStyle.Body1">Extract MIS data</RadzenText>
</RadzenStack>
</RadzenCard>
}
</EditForm>
</FilterVisibilityManager>
}
@code {
[Parameter]
public int? Id { get; set; }
[SupplyParameterFromQuery(Name = "copySearchId")]
public int? CopySearchId { get; set; }
private ClientSearchViewModel _search = new() { Criteria = new() };
private FilterVisibilityManager _visibilityManager = null!;
private bool _isLoading = true;
private bool _isSubmitting;
private string? _errorMessage;
protected override async Task OnInitializedAsync()
{
await LoadSearchAsync();
}
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 = FormatValidationErrors(validation.FieldErrors); },
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 = FormatValidationErrors(validation.FieldErrors); },
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()
};
}
}
finally
{
_isLoading = false;
}
}
private static string FormatValidationErrors(IReadOnlyDictionary<string, string[]> fieldErrors)
{
var messages = fieldErrors.SelectMany(kv => kv.Value);
return string.Join(" ", messages);
}
private void OnSearchTypeChanged(ValidCombination? combo)
{
StateHasChanged();
}
private void HandleSearchUpdate(SearchUpdateViewModel update)
{
_search.Status = update.Status;
_search.SubmitDt = update.SubmitDt;
_search.StartDt = update.StartDt;
_search.EndDt = update.EndDt;
StateHasChanged();
}
private async Task HandleValidSubmit()
{
var validationError = ValidationService.Validate(_search, _visibilityManager.SelectedSearchType, _visibilityManager);
if (!string.IsNullOrEmpty(validationError))
{
NotificationService.Notify(NotificationSeverity.Error, "Validation Error", validationError);
return;
}
await SubmitSearchInternalAsync();
}
private async Task SubmitSearchAsync()
{
var validationError = ValidationService.Validate(_search, _visibilityManager.SelectedSearchType, _visibilityManager);
if (!string.IsNullOrEmpty(validationError))
{
NotificationService.Notify(NotificationSeverity.Error, "Validation Error", validationError);
return;
}
await SubmitSearchInternalAsync();
}
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 SubmissionService.SubmitAsync(_search);
if (result.IsSuccess)
{
NavigationManager.NavigateTo($"/search/{result.SearchId}");
}
else
{
NotificationService.Notify(NotificationSeverity.Error, "Error", result.ErrorMessage);
}
}
finally
{
_isSubmitting = false;
}
}
private void CopySearchAsync()
{
NavigationManager.NavigateTo($"/search?copySearchId={_search.Id}");
}
private async Task DownloadResultsAsync()
{
var result = await SearchApi.GetResultsAsync(_search.Id);
result.Switch(
bytes =>
{
if (bytes.Length > 0)
{
_ = Task.Run(async () =>
{
try
{
await JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", bytes);
}
catch (JSException ex)
{
Console.WriteLine($"JS interop failed during file download: {ex.Message}");
}
});
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", FormatValidationErrors(validation.FieldErrors)); },
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
public void Dispose()
{
// SignalRStatusHandler handles its own disposal
}
}