@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; } }