@page "/search"
@page "/search/{Id:int}"
@attribute [Authorize]
@using JdeScoping.Core.ApiContracts
@using JdeScoping.Client.Extensions
@inject ISearchApiClient SearchApi
@inject IHubConnectionService HubConnection
@inject AuthStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject DialogService DialogService
@inject NotificationService NotificationService
@inject IJSRuntime JSRuntime
@implements IDisposable
@(_search.Id == 0 ? "New Search" : "Search") - JDE Scoping Tool
Search
@if (!_search.IsReadOnly)
{
}
@if (_isLoading)
{
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
@_errorMessage
}
else
{
@if (_search.IsReadOnly)
{
Note: Search is read-only because it has already been submitted. To change or re-run the search again click the Copy button.
}
Search Details
@if (_search.HasResults)
{
}
@if (_showTimespan)
{
}
@if (_showWorkOrder)
{
}
@if (_showItemNumber)
{
}
@if (_showProfitCenter)
{
}
@if (_showWorkCenter)
{
}
@if (_showComponentLot)
{
}
@if (_showOperator)
{
}
@if (_showItemOperationMis)
{
}
@if (_showExtractMis)
{
Extract MIS data
}
}
@code {
[Parameter]
public int? Id { get; set; }
[SupplyParameterFromQuery(Name = "copySearchId")]
public int? CopySearchId { get; set; }
private ClientSearchViewModel _search = new() { Criteria = new() };
private IReadOnlyList _validCombinations = ValidCombination.GetAll();
private int? _selectedSearchType;
private bool _isLoading = true;
private bool _isSubmitting;
private string? _errorMessage;
// Filter visibility flags
private bool _showTimespan;
private bool _showWorkOrder;
private bool _showItemNumber;
private bool _showProfitCenter;
private bool _showWorkCenter;
private bool _showComponentLot;
private bool _showOperator;
private bool _showItemOperationMis;
private bool _showExtractMis;
protected override async Task OnInitializedAsync()
{
await LoadSearchAsync();
await SetupSignalRAsync();
}
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
{
// New search
_search = new ClientSearchViewModel
{
Status = "New",
UserName = await AuthStateProvider.GetUsernameAsync() ?? "",
Criteria = new SearchCriteriaViewModel()
};
}
// Detect search type from criteria (only if no error)
if (string.IsNullOrEmpty(_errorMessage))
{
DetectSearchType();
}
}
finally
{
_isLoading = false;
}
}
private static string FormatValidationErrors(IReadOnlyDictionary fieldErrors)
{
var messages = fieldErrors.SelectMany(kv => kv.Value);
return string.Join(" ", messages);
}
private void DetectSearchType()
{
var criteria = _search.Criteria;
bool hasTimespan = criteria.MinimumDt.HasValue || criteria.MaximumDt.HasValue;
bool hasWorkOrder = criteria.WorkOrders.Count > 0;
bool hasItemNumber = criteria.Items.Count > 0;
bool hasProfitCenter = criteria.ProfitCenters.Count > 0;
bool hasWorkCenter = criteria.WorkCenters.Count > 0;
bool hasComponentLot = criteria.ComponentLots.Count > 0;
bool hasOperator = criteria.Operators.Count > 0;
bool hasPartOperation = criteria.PartOperations.Count > 0;
bool hasExtractMis = criteria.ExtractMisData;
foreach (var combo in _validCombinations)
{
if (combo.Matches(hasTimespan, hasWorkOrder, hasItemNumber, hasProfitCenter, hasWorkCenter, hasComponentLot, hasOperator, hasPartOperation, hasExtractMis))
{
_selectedSearchType = combo.Id;
UpdateFilterVisibility(combo);
break;
}
}
}
private void OnSearchTypeChanged()
{
var combo = _validCombinations.FirstOrDefault(c => c.Id == _selectedSearchType);
if (combo != null)
{
UpdateFilterVisibility(combo);
}
}
private void UpdateFilterVisibility(ValidCombination combo)
{
_showTimespan = combo.Timespan;
_showWorkOrder = combo.WorkOrder;
_showItemNumber = combo.ItemNumber;
_showProfitCenter = combo.ProfitCenter;
_showWorkCenter = combo.WorkCenter;
_showComponentLot = combo.ComponentLot;
_showOperator = combo.Operator;
_showItemOperationMis = combo.ItemOperationMis;
_showExtractMis = combo.ExtractMis;
// Set ExtractMisData flag based on combo
_search.Criteria.ExtractMisData = combo.ExtractMis;
}
private async Task SetupSignalRAsync()
{
HubConnection.OnSearchUpdate += HandleSearchUpdate;
await HubConnection.StartAsync();
}
private void HandleSearchUpdate(SearchUpdateViewModel update)
{
if (update.Id == _search.Id)
{
InvokeAsync(() =>
{
_search.Status = update.Status;
_search.SubmitDt = update.SubmitDt;
_search.StartDt = update.StartDt;
_search.EndDt = update.EndDt;
StateHasChanged();
});
}
}
private async Task HandleValidSubmit()
{
// DataAnnotationsValidator has already validated the model
// Now perform additional custom validation
if (_selectedSearchType == null)
{
NotificationService.Notify(NotificationSeverity.Error, "Validation Error", "Search type is required.");
return;
}
// Validate filter data based on search type
var validationError = ValidateFilters();
if (!string.IsNullOrEmpty(validationError))
{
NotificationService.Notify(NotificationSeverity.Error, "Filter Validation Error", validationError);
return;
}
await SubmitSearchInternalAsync();
}
private async Task SubmitSearchAsync()
{
// Manual submit button handler - validate and submit
if (string.IsNullOrWhiteSpace(_search.Name))
{
NotificationService.Notify(NotificationSeverity.Error, "Validation Error", "Name is required.");
return;
}
if (_selectedSearchType == null)
{
NotificationService.Notify(NotificationSeverity.Error, "Validation Error", "Search type is required.");
return;
}
// Validate filter data based on search type
var validationError = ValidateFilters();
if (!string.IsNullOrEmpty(validationError))
{
NotificationService.Notify(NotificationSeverity.Error, "Filter 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 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", 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); }
);
}
finally
{
_isSubmitting = false;
}
}
private string? ValidateFilters()
{
if (_showWorkOrder && _search.Criteria.WorkOrders.Count == 0)
return "At least one work order must be specified for the work order filter.";
if (_showItemNumber && _search.Criteria.Items.Count == 0)
return "At least one item number must be specified for the item number filter.";
if (_showProfitCenter && _search.Criteria.ProfitCenters.Count == 0)
return "At least one profit center must be specified for the profit center filter.";
if (_showWorkCenter && _search.Criteria.WorkCenters.Count == 0)
return "At least one work center must be specified for the work center filter.";
if (_showComponentLot && _search.Criteria.ComponentLots.Count == 0)
return "At least one component lot must be specified for the component lot filter.";
if (_showOperator && _search.Criteria.Operators.Count == 0)
return "At least one operator must be specified for the operator filter.";
if (_showItemOperationMis && _search.Criteria.PartOperations.Count == 0)
return "At least one item/operation/MIS entry must be specified for the MIS data filter.";
return null;
}
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)
{
_ = 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", 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()
{
HubConnection.OnSearchUpdate -= HandleSearchUpdate;
}
}