26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
30 KiB
30 KiB
Search Creation Page - New Implementation Guide
This document provides the implementation specification for the new search creation page (Search.razor / SearchCriteriaForm.razor) based on the legacy functionality analysis and the project's architecture choices.
Technology Stack
| Legacy | New | Notes |
|---|---|---|
| Kendo UI | Radzen Blazor | Free MIT license, replaces all Kendo components |
| jQuery FileUpload | Blazor InputFile | Native Blazor file upload component |
| EPPlus | ClosedXML | Free MIT license for Excel generation |
| SignalR 2.2.1 | ASP.NET Core SignalR | Modern SignalR with WithAutomaticReconnect() |
| Kendo Observable | Blazor Component State | Standard Blazor state management |
| jQuery | N/A | Not needed in Blazor |
| .NET Framework 4.8 | .NET 10 | Target framework |
Project Structure
Based on BlazorClient.md, the search functionality spans these files:
JdeScoping.Client/
├── Pages/
│ └── Search.razor # Main search page (routes to /search, /search/{id})
├── Components/
│ ├── SearchCriteriaForm.razor # Complex search form with all filter panels
│ ├── SearchStatusCard.razor # Real-time status display
│ └── LookupDropdown.razor # Reusable autocomplete wrapper
├── Services/
│ ├── SearchApiClient.cs # HTTP calls to SearchController
│ ├── LookupApiClient.cs # HTTP calls to LookupController
│ └── StatusHubClient.cs # SignalR connection
└── Models/
├── SearchViewModel.cs # Search details model
├── SearchCriteriaViewModel.cs # Filter criteria model
└── ValidCombination.cs # Search type definitions
Radzen Component Mapping
| Legacy Kendo | Radzen Replacement | Usage |
|---|---|---|
DropDownList |
RadzenDropDown |
Search Type selection |
ComboBox (autocomplete) |
RadzenAutoComplete |
Item, Profit Center, Work Center, Operator lookup |
DatePicker |
RadzenDatePicker |
Min/Max date selection |
Grid |
RadzenDataGrid |
Display filter data collections |
Button |
RadzenButton |
Submit, Clear, Add, Delete actions |
Alert |
RadzenNotification |
Validation error messages |
Window (confirm) |
RadzenDialog |
Confirmation dialogs |
Validator |
EditForm + DataAnnotationsValidator |
Form validation |
ProgressBar |
RadzenProgressBar |
Loading indicators |
Page Structure
Search.razor (Page)
@page "/search"
@page "/search/{Id:int?}"
@inject SearchApiClient SearchApi
@inject StatusHubClient StatusHub
@inject NavigationManager Navigation
<PageTitle>Search</PageTitle>
<RadzenCard>
<h2>
Search
@if (!IsReadOnly)
{
<RadzenButton Text="Submit"
Click="@OnSubmit"
ButtonStyle="ButtonStyle.Primary"
Size="ButtonSize.Small" />
}
</h2>
@if (IsReadOnly)
{
<RadzenAlert AlertStyle="AlertStyle.Warning" ShowIcon="true">
Search is read-only because it has already been submitted.
<RadzenButton Text="Copy" Click="@OnCopy" />
</RadzenAlert>
}
<SearchCriteriaForm @ref="criteriaForm"
ViewModel="@viewModel"
IsReadOnly="@IsReadOnly"
OnValidSubmit="@OnValidSubmit" />
</RadzenCard>
SearchCriteriaForm.razor (Component)
Contains all filter panels with conditional visibility based on selected search type.
Search Details Panel
| Field | Radzen Component | Binding |
|---|---|---|
| Search Type | RadzenDropDown<ValidCombination> |
@bind-Value="ViewModel.SearchType" with Change event |
| Name | RadzenTextBox |
@bind-Value="ViewModel.Name" |
| Submitted At | RadzenTextBox (ReadOnly) |
Value="@ViewModel.SubmitDT?.ToString("MM/dd/yyyy hh:mm:ss tt")" |
| Started At | RadzenTextBox (ReadOnly) |
Value="@ViewModel.StartDT?.ToString(...)" |
| Completed At | RadzenTextBox (ReadOnly) |
Value="@ViewModel.EndDT?.ToString(...)" |
| User | RadzenTextBox (ReadOnly) |
Value="@ViewModel.UserName" |
| Status | RadzenTextBox (ReadOnly) |
Value="@ViewModel.Status" with conditional Style |
| Download Results | RadzenButton |
Visible="@ViewModel.HasResults" |
Status Styling
private string GetStatusStyle() => ViewModel.Status == SearchStatus.Error
? "background-color: #FF6347;"
: "background-color: #eee;";
Valid Search Type Combinations
Defined in ValidCombination.cs as a static list:
public record ValidCombination(
int Id,
string Name,
bool Timespan,
bool WorkOrder,
bool ItemNumber,
bool ProfitCenter,
bool WorkCenter,
bool ComponentLot,
bool Operator,
bool ItemOperationMis,
bool ExtractMis);
public static class ValidCombinations
{
public static readonly List<ValidCombination> All = new()
{
new(10, "Work Order", false, true, false, false, false, false, false, false, false),
new(20, "Component Lot", false, false, false, false, false, true, false, false, false),
new(30, "Time Span + Profit Center", true, false, false, true, false, false, false, false, false),
new(40, "Time Span + Work Center", true, false, false, false, true, false, false, false, false),
new(50, "Time Span + Operator", true, false, false, false, false, false, true, false, false),
new(60, "Time Span + Profit Center + Item Number", true, false, true, true, false, false, false, false, false),
new(70, "Time Span + Profit Center + Item/Operation/MIS", true, false, false, true, false, false, false, true, false),
new(80, "Time Span + Profit Center + Work Order + Item/Operation/MIS", true, true, false, true, false, false, false, true, false),
new(90, "Time Span + Profit Center + Extract MIS", true, false, false, true, false, false, false, false, true),
new(100, "Time Span + Work Center + Item Number", true, false, true, false, true, false, false, false, false),
new(110, "Time Span + Work Center + Extract MIS", true, false, false, false, true, false, false, false, true),
new(120, "Time Span + Work Center + Item/Operation/MIS", true, false, false, false, true, false, false, true, false),
new(130, "Time Span + Work Center + Work Order + Item/Operation/MIS", true, true, false, false, true, false, false, true, false),
new(140, "Time Span + Item Number", true, false, true, false, false, false, false, false, false),
new(150, "Time Span + Work Center + Operator", true, false, false, false, true, false, true, false, false),
new(160, "Time Span + Profit Center + Operator", true, false, false, true, false, false, true, false, false)
};
}
Filter Panels
1. Time Span Filter
@if (ViewModel.SearchType?.Timespan == true)
{
<RadzenCard>
<RadzenText TextStyle="TextStyle.H6">Filter by timespan</RadzenText>
<div class="row">
<div class="col-md-5">
<RadzenLabel Text="Min Date" />
<RadzenDatePicker @bind-Value="ViewModel.MinimumDT"
Min="@(new DateTime(2002, 11, 1))"
Max="@(ViewModel.MaximumDT ?? DateTime.Today)"
Disabled="@IsReadOnly" />
</div>
<div class="col-md-5 offset-md-1">
<RadzenLabel Text="Max Date" />
<RadzenDatePicker @bind-Value="ViewModel.MaximumDT"
Min="@(ViewModel.MinimumDT ?? new DateTime(2002, 11, 1))"
Max="@DateTime.Today"
Disabled="@IsReadOnly" />
</div>
</div>
</RadzenCard>
}
2. Work Order Filter (with file upload/download)
@if (ViewModel.SearchType?.WorkOrder == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter by work order</RadzenText>
@if (!IsReadOnly)
{
<div class="btn-group">
<RadzenButton Text="Download Template"
Click="@DownloadWorkOrderTemplate"
ButtonStyle="ButtonStyle.Light" />
<InputFile OnChange="@UploadWorkOrders" accept=".xlsx" />
<RadzenButton Text="Clear Data"
Click="@ClearWorkOrders"
ButtonStyle="ButtonStyle.Light" />
</div>
}
</div>
<RadzenDataGrid Data="@ViewModel.WorkOrders" TItem="WorkOrderViewModel">
<Columns>
<RadzenDataGridColumn TItem="WorkOrderViewModel" Property="WorkOrderNumber" Title="Work Order Number" />
<RadzenDataGridColumn TItem="WorkOrderViewModel" Property="ItemNumber" Title="Item Number" />
</Columns>
</RadzenDataGrid>
<RadzenText><strong># of work orders:</strong> @ViewModel.WorkOrders.Count</RadzenText>
</RadzenCard>
}
3. Item Number Filter (with autocomplete + file upload)
@if (ViewModel.SearchType?.ItemNumber == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter by item number</RadzenText>
@if (!IsReadOnly)
{
<div class="btn-group">
<RadzenButton Text="Download Template" Click="@DownloadItemTemplate" />
<InputFile OnChange="@UploadItems" accept=".xlsx" />
<RadzenButton Text="Clear Data" Click="@ClearItems" />
</div>
}
</div>
@if (!IsReadOnly)
{
<div class="form-group">
<RadzenLabel Text="Item Number" />
<RadzenAutoComplete @bind-Value="selectedItemText"
Data="@itemSearchResults"
TextProperty="ItemNumber"
MinLength="3"
LoadData="@SearchItems"
Placeholder="Type 3+ characters to search..."
Style="width: 550px;" />
<RadzenButton Text="Add to filter"
Click="@AddSelectedItem"
Visible="@(selectedItem != null)" />
</div>
}
<RadzenDataGrid Data="@ViewModel.Items" TItem="ItemViewModel">
<Columns>
<RadzenDataGridColumn TItem="ItemViewModel" Property="ItemNumber" Title="Item Number" />
<RadzenDataGridColumn TItem="ItemViewModel" Property="Description" Title="Description" />
@if (!IsReadOnly)
{
<RadzenDataGridColumn TItem="ItemViewModel" Width="100px" Title="Actions">
<Template Context="item">
<RadzenButton Text="Delete" Click="@(() => DeleteItem(item))" />
</Template>
</RadzenDataGridColumn>
}
</Columns>
</RadzenDataGrid>
<RadzenText><strong># of item numbers:</strong> @ViewModel.Items.Count</RadzenText>
</RadzenCard>
}
4-5. Profit Center / Work Center Filters (autocomplete only, no file upload)
@if (ViewModel.SearchType?.ProfitCenter == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter by profit center</RadzenText>
@if (!IsReadOnly)
{
<RadzenButton Text="Clear Data" Click="@ClearProfitCenters" />
}
</div>
@if (!IsReadOnly)
{
<LookupDropdown TItem="ProfitCenterViewModel"
TextProperty="Code"
SearchEndpoint="@LookupApi.SearchProfitCentersAsync"
OnItemSelected="@AddProfitCenter"
Placeholder="Type 3+ characters to search..." />
}
<RadzenDataGrid Data="@ViewModel.ProfitCenters" TItem="ProfitCenterViewModel">
<Columns>
<RadzenDataGridColumn TItem="ProfitCenterViewModel" Property="Code" Title="Profit Center" />
<RadzenDataGridColumn TItem="ProfitCenterViewModel" Property="Description" Title="Description" />
@if (!IsReadOnly)
{
<RadzenDataGridColumn TItem="ProfitCenterViewModel" Width="100px" Title="Actions">
<Template Context="item">
<RadzenButton Text="Delete" Click="@(() => DeleteProfitCenter(item))" />
</Template>
</RadzenDataGridColumn>
}
</Columns>
</RadzenDataGrid>
</RadzenCard>
}
Work Center filter follows the same pattern with WorkCenterViewModel.
6. Component Lot Filter (file upload only)
@if (ViewModel.SearchType?.ComponentLot == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter by component lot</RadzenText>
@if (!IsReadOnly)
{
<div class="btn-group">
<RadzenButton Text="Download Template" Click="@DownloadComponentLotTemplate" />
<InputFile OnChange="@UploadComponentLots" accept=".xlsx" />
<RadzenButton Text="Clear Data" Click="@ClearComponentLots" />
</div>
}
</div>
<RadzenDataGrid Data="@ViewModel.ComponentLots" TItem="LotViewModel">
<Columns>
<RadzenDataGridColumn TItem="LotViewModel" Property="LotNumber" Title="Lot Number" />
<RadzenDataGridColumn TItem="LotViewModel" Property="ItemNumber" Title="Item Number" />
</Columns>
</RadzenDataGrid>
<RadzenText><strong># of component lots:</strong> @ViewModel.ComponentLots.Count</RadzenText>
</RadzenCard>
}
7. Operator Filter (autocomplete only)
@if (ViewModel.SearchType?.Operator == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter by operator</RadzenText>
@if (!IsReadOnly)
{
<RadzenButton Text="Clear Data" Click="@ClearOperators" />
}
</div>
@if (!IsReadOnly)
{
<LookupDropdown TItem="JdeUserViewModel"
TextProperty="FullName"
SearchEndpoint="@LookupApi.SearchOperatorsAsync"
OnItemSelected="@AddOperator"
Placeholder="Type 3+ characters to search..." />
}
<RadzenDataGrid Data="@ViewModel.Operators" TItem="JdeUserViewModel">
<Columns>
<RadzenDataGridColumn TItem="JdeUserViewModel" Property="AddressNumber" Title="Address Number" />
<RadzenDataGridColumn TItem="JdeUserViewModel" Property="UserID" Title="User Name" />
<RadzenDataGridColumn TItem="JdeUserViewModel" Property="FullName" Title="Full Name" />
@if (!IsReadOnly)
{
<RadzenDataGridColumn TItem="JdeUserViewModel" Width="100px" Title="Actions">
<Template Context="item">
<RadzenButton Text="Delete" Click="@(() => DeleteOperator(item))" />
</Template>
</RadzenDataGridColumn>
}
</Columns>
</RadzenDataGrid>
</RadzenCard>
}
8. Item/Operation/MIS Filter (file upload only)
@if (ViewModel.SearchType?.ItemOperationMis == true)
{
<RadzenCard>
<div class="d-flex justify-content-between align-items-center">
<RadzenText TextStyle="TextStyle.H6">Filter By Item/Operation/MIS</RadzenText>
@if (!IsReadOnly)
{
<div class="btn-group">
<RadzenButton Text="Download Template" Click="@DownloadPartOperationTemplate" />
<InputFile OnChange="@UploadPartOperations" accept=".xlsx" />
<RadzenButton Text="Clear Data" Click="@ClearPartOperations" />
</div>
}
</div>
<RadzenDataGrid Data="@ViewModel.PartOperations" TItem="PartOperationViewModel">
<Columns>
<RadzenDataGridColumn TItem="PartOperationViewModel" Property="ItemNumber" Title="Item Number" />
<RadzenDataGridColumn TItem="PartOperationViewModel" Property="OperationNumber" Title="Operation Step Number" />
<RadzenDataGridColumn TItem="PartOperationViewModel" Property="MisNumber" Title="MIS Number" />
<RadzenDataGridColumn TItem="PartOperationViewModel" Property="MisRevision" Title="MIS Revision" />
</Columns>
</RadzenDataGrid>
<RadzenText><strong># of item / operations:</strong> @ViewModel.PartOperations.Count</RadzenText>
</RadzenCard>
}
9. Extract MIS Data Option
@if (ViewModel.SearchType?.ExtractMis == true)
{
<RadzenCard>
<RadzenCheckBox @bind-Value="@extractMisChecked"
Disabled="true"
Name="ExtractMisData" />
<RadzenLabel Text="Extract MIS data" Component="ExtractMisData" />
</RadzenCard>
}
@code {
private bool extractMisChecked = true; // Always true when this panel is visible
}
API Clients
SearchApiClient.cs
public class SearchApiClient
{
private readonly HttpClient _http;
public SearchApiClient(HttpClient http) => _http = http;
public async Task<SearchViewModel?> GetSearchAsync(int? id) =>
await _http.GetFromJsonAsync<SearchViewModel>($"api/search/{id}");
public async Task<SearchViewModel?> CopySearchAsync(int id) =>
await _http.GetFromJsonAsync<SearchViewModel>($"api/search/{id}/copy");
public async Task<int> SaveAsync(SearchViewModel viewModel) =>
await _http.PostAsJsonAsync("api/search", viewModel)
.Result.Content.ReadFromJsonAsync<int>();
public async Task<byte[]> GetResultsAsync(int id) =>
await _http.GetByteArrayAsync($"api/search/{id}/results");
}
LookupApiClient.cs
public class LookupApiClient
{
private readonly HttpClient _http;
public LookupApiClient(HttpClient http) => _http = http;
public async Task<IEnumerable<ItemViewModel>> SearchItemsAsync(string query) =>
await _http.GetFromJsonAsync<IEnumerable<ItemViewModel>>($"api/lookup/items?q={query}")
?? Enumerable.Empty<ItemViewModel>();
public async Task<IEnumerable<ProfitCenterViewModel>> SearchProfitCentersAsync(string query) =>
await _http.GetFromJsonAsync<IEnumerable<ProfitCenterViewModel>>($"api/lookup/profitcenters?q={query}")
?? Enumerable.Empty<ProfitCenterViewModel>();
public async Task<IEnumerable<WorkCenterViewModel>> SearchWorkCentersAsync(string query) =>
await _http.GetFromJsonAsync<IEnumerable<WorkCenterViewModel>>($"api/lookup/workcenters?q={query}")
?? Enumerable.Empty<WorkCenterViewModel>();
public async Task<IEnumerable<JdeUserViewModel>> SearchOperatorsAsync(string query) =>
await _http.GetFromJsonAsync<IEnumerable<JdeUserViewModel>>($"api/lookup/operators?q={query}")
?? Enumerable.Empty<JdeUserViewModel>();
}
File I/O with ClosedXML
FileIOController.cs (Server-side)
using ClosedXML.Excel;
[ApiController]
[Route("api/fileio")]
public class FileIOController : ControllerBase
{
[HttpPost("workorders/upload")]
public async Task<ActionResult<FileUploadResult<WorkOrderViewModel>>> UploadWorkOrders(IFormFile file)
{
using var stream = file.OpenReadStream();
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet(1);
var workOrderNumbers = new List<long>();
foreach (var row in worksheet.RowsUsed().Skip(1)) // Skip header
{
if (long.TryParse(row.Cell(1).GetString().Trim(), out var num))
workOrderNumbers.Add(num);
}
var validated = await _db.LookupWorkOrdersAsync(workOrderNumbers);
return Ok(new FileUploadResult<WorkOrderViewModel>
{
WasSuccessful = true,
Data = validated
});
}
[HttpPost("workorders/download")]
public IActionResult DownloadWorkOrders([FromBody] List<long> workOrders)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Work Orders");
worksheet.Cell(1, 1).Value = "Work Order Number";
for (int i = 0; i < workOrders.Count; i++)
worksheet.Cell(i + 2, 1).Value = workOrders[i];
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"work_order_template.xlsx");
}
// Similar methods for PartNumbers, ComponentLots, PartOperations...
}
Blazor File Upload Handler
private async Task UploadWorkOrders(InputFileChangeEventArgs e)
{
var file = e.File;
using var content = new MultipartFormDataContent();
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); // 10MB max
content.Add(new StreamContent(stream), "file", file.Name);
var response = await Http.PostAsync("api/fileio/workorders/upload", content);
var result = await response.Content.ReadFromJsonAsync<FileUploadResult<WorkOrderViewModel>>();
if (result?.WasSuccessful == true)
{
ViewModel.WorkOrders = result.Data.ToList();
}
else
{
NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", result?.ErrorMessage);
}
}
Blazor File Download Handler
private async Task DownloadWorkOrderTemplate()
{
var workOrderNumbers = ViewModel.WorkOrders.Select(w => w.WorkOrderNumber).ToList();
var response = await Http.PostAsJsonAsync("api/fileio/workorders/download", workOrderNumbers);
var bytes = await response.Content.ReadAsByteArrayAsync();
// Use JS interop to trigger download
await JSRuntime.InvokeVoidAsync("downloadFile", "work_order_template.xlsx", bytes);
}
wwwroot/js/download.js:
window.downloadFile = (fileName, byteArray) => {
const blob = new Blob([new Uint8Array(byteArray)]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
};
SignalR Integration
StatusHubClient.cs
public class StatusHubClient : IAsyncDisposable
{
private HubConnection? _connection;
public event Action<SearchStatusUpdate>? OnStatusChanged;
public async Task ConnectAsync(string baseUrl)
{
_connection = new HubConnectionBuilder()
.WithUrl($"{baseUrl}/hubs/status")
.WithAutomaticReconnect()
.Build();
_connection.On<SearchStatusUpdate>("StatusChanged", update =>
{
OnStatusChanged?.Invoke(update);
});
await _connection.StartAsync();
}
public async ValueTask DisposeAsync()
{
if (_connection != null)
await _connection.DisposeAsync();
}
}
Usage in Search.razor
@implements IAsyncDisposable
@code {
protected override async Task OnInitializedAsync()
{
StatusHub.OnStatusChanged += HandleStatusChanged;
await StatusHub.ConnectAsync(Navigation.BaseUri);
}
private void HandleStatusChanged(SearchStatusUpdate update)
{
if (update.Id == viewModel?.ID)
{
viewModel.SubmitDT = update.SubmitDT;
viewModel.StartDT = update.StartDT;
viewModel.EndDT = update.EndDT;
viewModel.Status = update.Status;
viewModel.HasResults = update.Status == SearchStatus.Ended;
StateHasChanged();
}
}
public async ValueTask DisposeAsync()
{
StatusHub.OnStatusChanged -= HandleStatusChanged;
await StatusHub.DisposeAsync();
}
}
Validation
Form-Level Validation with EditForm
<EditForm Model="@ViewModel" OnValidSubmit="@OnValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<!-- Form content -->
</EditForm>
SearchViewModel Validation Attributes
public class SearchViewModel
{
public int? ID { get; set; }
[Required(ErrorMessage = "Name is required.")]
public string? Name { get; set; }
public string? UserName { get; set; }
[Required(ErrorMessage = "Search Type is required.")]
public ValidCombination? SearchType { get; set; }
public SearchStatus Status { get; set; }
// ... other properties
}
Custom Filter Validation (Submit Handler)
private async Task OnValidSubmit()
{
// Filter-level validation
if (ViewModel.SearchType?.WorkOrder == true && !ViewModel.WorkOrders.Any())
{
NotificationService.Notify(NotificationSeverity.Error,
"Validation Error",
"At least one work order must be specified for the work order filter.");
return;
}
if (ViewModel.SearchType?.ItemNumber == true && !ViewModel.Items.Any())
{
NotificationService.Notify(NotificationSeverity.Error,
"Validation Error",
"At least one item number must be specified for the item number filter.");
return;
}
// Similar checks for ProfitCenters, WorkCenters, ComponentLots, Operators, PartOperations
// Confirmation dialog
var confirmed = await DialogService.Confirm(
"Are you sure you want to submit the search?",
"Confirm Submit",
new ConfirmOptions { OkButtonText = "OK", CancelButtonText = "Cancel" });
if (confirmed == true)
{
await SubmitSearch();
}
}
Confirmation Dialogs
Using RadzenDialog service:
@inject DialogService DialogService
private async Task ClearWorkOrders()
{
var confirmed = await DialogService.Confirm(
"Are you sure you want to clear all work orders?",
"Action Confirmation",
new ConfirmOptions { OkButtonText = "OK", CancelButtonText = "Cancel" });
if (confirmed == true)
{
ViewModel.WorkOrders.Clear();
}
}
API Endpoints Summary (New)
Search Operations
| Endpoint | Method | Purpose |
|---|---|---|
api/search/{id?} |
GET | Get search by ID or create blank |
api/search/{id}/copy |
GET | Copy existing search |
api/search |
POST | Save search criteria |
api/search/{id}/results |
GET | Download Excel results |
Lookup/Autocomplete
| Endpoint | Method | Purpose |
|---|---|---|
api/lookup/items?q= |
GET | Search items |
api/lookup/profitcenters?q= |
GET | Search profit centers |
api/lookup/workcenters?q= |
GET | Search work centers |
api/lookup/operators?q= |
GET | Search operators |
File I/O
| Endpoint | Method | Purpose |
|---|---|---|
api/fileio/workorders/upload |
POST | Upload work order Excel |
api/fileio/workorders/download |
POST | Download work order template |
api/fileio/items/upload |
POST | Upload item Excel |
api/fileio/items/download |
POST | Download item template |
api/fileio/componentlots/upload |
POST | Upload component lot Excel |
api/fileio/componentlots/download |
POST | Download component lot template |
api/fileio/partoperations/upload |
POST | Upload part operation Excel |
api/fileio/partoperations/download |
POST | Download part operation template |
Status Values
| Status | Description | UI Behavior |
|---|---|---|
New |
Not yet submitted | Editable mode |
Queued |
Waiting to be processed | Read-only mode |
Running |
Currently processing | Read-only mode |
Ended |
Completed successfully | Read-only mode, Download Results visible |
Error |
Failed | Read-only mode, Status field has red background |
Removed/Deprecated
- CheckCamstar_Flag: Not migrated (dead code in legacy)
- jQuery FileUpload: Replaced with Blazor
InputFile - Kendo Observable: Replaced with Blazor component state
- EPPlus: Replaced with ClosedXML (MIT license)
- iframe download trick: Replaced with JS interop blob download