@* 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 @(_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. } @if (_visibilityManager.ShowTimespan) { } @if (_visibilityManager.ShowWorkOrder) { } @if (_visibilityManager.ShowItemNumber) { } @if (_visibilityManager.ShowProfitCenter) { } @if (_visibilityManager.ShowWorkCenter) { } @if (_visibilityManager.ShowComponentLot) { } @if (_visibilityManager.ShowOperator) { } @if (_visibilityManager.ShowItemOperationMis) { } @if (_visibilityManager.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 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 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 } }