Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,94 @@
# Blazor Client
The `JdeScoping.Client` project is a Blazor WebAssembly application using Radzen Blazor components.
## Project Structure
```
JdeScoping.Client/
├── Program.cs # WASM entry, configure HttpClient + SignalR
├── Pages/
│ ├── Login.razor # LDAP login form
│ ├── Search.razor # Main search criteria page
│ ├── Results.razor # Search history + download
│ └── Admin.razor # Data sync status (optional)
├── Components/
│ ├── SearchCriteriaForm.razor # Complex search form
│ ├── SearchStatusCard.razor # Real-time status display
│ └── LookupDropdown.razor # Autocomplete wrapper
├── Services/
│ ├── SearchApiClient.cs # HTTP calls to SearchController
│ ├── LookupApiClient.cs # HTTP calls to LookupController
│ ├── AuthApiClient.cs # Login/logout
│ └── StatusHubClient.cs # SignalR connection
└── wwwroot/
└── css/ # Custom styles if needed
```
## Radzen Components
Radzen Blazor replaces the legacy Kendo UI JS components. The core library is free (MIT license).
| Component | Usage |
|-----------|-------|
| `RadzenDataGrid` | Search results and history tables |
| `RadzenDropDown` | Work center, operator selection |
| `RadzenAutoComplete` | Item number lookup with search |
| `RadzenDatePicker` | Date range selection |
| `RadzenButton` | Form actions |
| `RadzenCard` | Layout containers |
| `RadzenNotification` | Toast messages |
| `RadzenProgressBar` | Search progress indication |
## Program.cs Configuration
```csharp
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// HTTP client for API calls
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// API clients
builder.Services.AddScoped<SearchApiClient>();
builder.Services.AddScoped<LookupApiClient>();
builder.Services.AddScoped<AuthApiClient>();
builder.Services.AddScoped<StatusHubClient>();
await builder.Build().RunAsync();
```
## SignalR Client
The `StatusHubClient` connects on login and subscribes to status updates:
```csharp
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();
}
}
```
## Related Documentation
- [Solution Structure](./SolutionStructure.md)
- [Data Flow](./DataFlow.md)
- [Dependencies](./Dependencies.md)
+225
View File
@@ -0,0 +1,225 @@
# Configuration
The application uses standard ASP.NET Core configuration with `appsettings.json` and environment variables for sensitive values.
## appsettings.json Structure
```json
{
"ConnectionStrings": {
"SqlServer": "Server=localhost;Database=LotFinder;Integrated Security=true;TrustServerCertificate=true",
"JdeOracle": "Data Source=jde-server:1521/JDEPROD;User Id=${JDE_USER};Password=${JDE_PASSWORD}",
"CmsOracle": "Data Source=cms-server:1521/CMSPROD;User Id=${CMS_USER};Password=${CMS_PASSWORD}"
},
"DataSource": {
"UseFileDataSource": false,
"FileDirectory": "DevData"
},
"Auth": {
"UseFakeAuth": false
},
"Ldap": {
"Url": "LDAP://your-domain.com",
"BaseDn": "DC=your-domain,DC=com",
"RequiredGroup": "CN=LotFinderUsers,OU=Groups,DC=your-domain,DC=com"
},
"DataSync": {
"MassSchedule": "0 2 * * 0",
"DailySchedule": "0 3 * * *",
"HourlySchedule": "0 * * * *",
"BatchSize": 10000
},
"Search": {
"MaxResultRows": 100000,
"TimeoutSeconds": 300,
"MaxConcurrentSearches": 5
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
```
## Environment Variables
Sensitive values are provided via environment variables at runtime:
| Variable | Purpose |
|----------|---------|
| `JDE_USER` | JDE Oracle username |
| `JDE_PASSWORD` | JDE Oracle password |
| `CMS_USER` | CMS Oracle username |
| `CMS_PASSWORD` | CMS Oracle password |
For local development, use User Secrets or a `.env` file (not committed to source control).
## Strongly-Typed Options
Configuration sections are bound to strongly-typed options classes:
```csharp
public class LdapOptions
{
public string Url { get; set; }
public string BaseDn { get; set; }
public string RequiredGroup { get; set; }
}
public class DataSyncOptions
{
public string MassSchedule { get; set; }
public string DailySchedule { get; set; }
public string HourlySchedule { get; set; }
public int BatchSize { get; set; } = 10000;
}
public class SearchOptions
{
public int MaxResultRows { get; set; } = 100000;
public int TimeoutSeconds { get; set; } = 300;
public int MaxConcurrentSearches { get; set; } = 5;
}
public class DataSourceOptions
{
public bool UseFileDataSource { get; set; } = false;
public string FileDirectory { get; set; } = "DevData";
}
public class AuthOptions
{
public bool UseFakeAuth { get; set; } = false;
}
```
Registered in `Program.cs`:
```csharp
builder.Services.Configure<LdapOptions>(builder.Configuration.GetSection("Ldap"));
builder.Services.Configure<DataSyncOptions>(builder.Configuration.GetSection("DataSync"));
builder.Services.Configure<SearchOptions>(builder.Configuration.GetSection("Search"));
builder.Services.Configure<DataSourceOptions>(builder.Configuration.GetSection("DataSource"));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Auth"));
```
## Data Source Configuration
The JDE and CMS data sources support two implementations:
| Implementation | Use Case |
|----------------|----------|
| Oracle (`JdeOracleDataSource`, `CmsOracleDataSource`) | Production - connects to Oracle databases |
| File (`JdeFileDataSource`, `CmsFileDataSource`) | Development - reads from exported JSON/CSV files |
### Development Setup
For development without Oracle access, set `UseFileDataSource: true` in `appsettings.Development.json`:
```json
{
"DataSource": {
"UseFileDataSource": true,
"FileDirectory": "DevData"
}
}
```
Place data export files in the `DevData` directory:
```
DevData/
├── workorders.json
├── lots.json
├── items.json
└── lotusage.json
```
### Registration Logic
```csharp
var dataSourceOptions = builder.Configuration
.GetSection("DataSource").Get<DataSourceOptions>();
if (dataSourceOptions?.UseFileDataSource == true || builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IJdeDataSource, JdeFileDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsFileDataSource>();
}
else
{
builder.Services.AddScoped<IJdeDataSource, JdeOracleDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsOracleDataSource>();
}
```
## Authentication Configuration
Authentication supports two implementations:
| Implementation | Use Case |
|----------------|----------|
| LDAP (`LdapAuthService`) | Production - authenticates against real LDAP server |
| Fake (`FakeAuthService`) | Development - accepts any non-empty credentials |
### Development Setup
For development without LDAP access, set `UseFakeAuth: true` in `appsettings.Development.json`:
```json
{
"Auth": {
"UseFakeAuth": true
}
}
```
The fake auth service:
- Accepts any non-empty username/password combination
- Returns the username as the display name
- Always returns `true` for group membership checks
### Registration Logic
```csharp
var authOptions = builder.Configuration
.GetSection("Auth").Get<AuthOptions>();
if (authOptions?.UseFakeAuth == true)
{
builder.Services.AddScoped<IAuthService, FakeAuthService>();
}
else
{
builder.Services.AddScoped<IAuthService, LdapAuthService>();
}
```
## Cron Expressions
Data sync schedules use cron expressions, parsed by the Cronos library:
| Expression | Meaning |
|------------|---------|
| `0 2 * * 0` | Sunday at 2:00 AM |
| `0 3 * * *` | Daily at 3:00 AM |
| `0 * * * *` | Every hour on the hour |
## Windows Service Installation
When installing as a Windows Service, environment variables can be set:
```powershell
# Create service
sc.exe create JdeScopingTool binPath= "C:\Services\JdeScoping\JdeScoping.Host.exe"
# Set environment variables for the service
$envVars = "JDE_USER=myuser`0JDE_PASSWORD=mypass`0CMS_USER=cmsuser`0CMS_PASSWORD=cmspass"
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\JdeScopingTool" -Name "Environment" -Value $envVars
```
## Related Documentation
- [Host Project](./HostProject.md)
- [Data Flow](./DataFlow.md)
+277
View File
@@ -0,0 +1,277 @@
# Core Project
The `JdeScoping.Core` project contains business logic and data access. It has no ASP.NET dependencies and is fully testable.
## Project Structure
```
JdeScoping.Core/
├── Models/
│ ├── Search.cs # Search request + status
│ ├── SearchCriteria.cs # Search parameters
│ ├── SearchStatus.cs # Status enum
│ ├── WorkOrder.cs # Work order entity
│ ├── Lot.cs # Lot entity
│ ├── Item.cs # Item entity
│ └── DataUpdate.cs # Sync tracking
├── Interfaces/
│ ├── ISearchRepository.cs # Local SQL Server cache
│ ├── IJdeDataSource.cs # JDE data access (interface)
│ ├── ICmsDataSource.cs # CMS data access (interface)
│ ├── ISearchService.cs # Search execution
│ └── IExcelExportService.cs # Excel generation
├── Repositories/
│ ├── SearchRepository.cs # Dapper against SQL Server
│ ├── Jde/
│ │ ├── JdeOracleDataSource.cs # Production: Oracle connection
│ │ └── JdeFileDataSource.cs # Development: File-based data
│ └── Cms/
│ ├── CmsOracleDataSource.cs # Production: Oracle connection
│ └── CmsFileDataSource.cs # Development: File-based data
├── Services/
│ ├── SearchService.cs # Search execution logic
│ ├── ExcelExportService.cs # ClosedXML generation
│ └── DataSyncOrchestrator.cs # Sync orchestration logic
└── Auth/
├── IAuthService.cs # Authentication interface
├── LdapAuthService.cs # Production: Real LDAP server
└── FakeAuthService.cs # Development: Accepts any credentials
```
## Repository Pattern
Repositories use Dapper for data access. Connections are created per-query and disposed after use.
```csharp
public class SearchRepository : ISearchRepository
{
private readonly string _connectionString;
public SearchRepository(IConfiguration config)
{
_connectionString = config.GetConnectionString("SqlServer");
}
public async Task<Search> GetByIdAsync(int id)
{
using var connection = new SqlConnection(_connectionString);
return await connection.QuerySingleOrDefaultAsync<Search>(
"SELECT * FROM Search WHERE Id = @Id", new { Id = id });
}
public async Task<int> CreateAsync(Search search)
{
using var connection = new SqlConnection(_connectionString);
return await connection.ExecuteScalarAsync<int>(
@"INSERT INTO Search (UserId, Criteria, Status, CreatedAt)
VALUES (@UserId, @Criteria, @Status, @CreatedAt);
SELECT SCOPE_IDENTITY();", search);
}
}
```
## Data Source Pattern (JDE/CMS)
JDE and CMS data access uses an interface with two implementations: production (Oracle) and development (file-based). This allows development without Oracle connectivity.
### Interface Definition
```csharp
public interface IJdeDataSource
{
Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since);
Task<IEnumerable<Lot>> GetLotsAsync(DateTime since);
Task<IEnumerable<Item>> GetItemsAsync();
// ... other data retrieval methods
}
public interface ICmsDataSource
{
Task<IEnumerable<LotUsage>> GetLotUsageAsync(DateTime since);
// ... other CMS data methods
}
```
### Production Implementation (Oracle)
```csharp
public class JdeOracleDataSource : IJdeDataSource
{
private readonly string _connectionString;
public JdeOracleDataSource(IConfiguration config)
{
_connectionString = config.GetConnectionString("JdeOracle");
}
public async Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since)
{
using var connection = new OracleConnection(_connectionString);
return await connection.QueryAsync<WorkOrder>(
"SELECT * FROM F4801 WHERE UPMJ >= :Since", new { Since = since });
}
}
```
### Development Implementation (File-based)
```csharp
public class JdeFileDataSource : IJdeDataSource
{
private readonly string _dataDirectory;
public JdeFileDataSource(IConfiguration config)
{
_dataDirectory = config["DataSource:FileDirectory"] ?? "DevData";
}
public async Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since)
{
var filePath = Path.Combine(_dataDirectory, "workorders.json");
var json = await File.ReadAllTextAsync(filePath);
var allOrders = JsonSerializer.Deserialize<List<WorkOrder>>(json);
return allOrders.Where(wo => wo.UpdateDate >= since);
}
}
```
### Registration by Environment
```csharp
// In Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IJdeDataSource, JdeFileDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsFileDataSource>();
}
else
{
builder.Services.AddScoped<IJdeDataSource, JdeOracleDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsOracleDataSource>();
}
```
This pattern enables:
- Development without Oracle database access
- Testing with predictable data sets
- Easy switching between implementations via configuration
## Authentication Pattern
Authentication uses the same interface pattern with production and development implementations.
### Interface Definition
```csharp
public interface IAuthService
{
Task<AuthResult> AuthenticateAsync(string username, string password);
Task<bool> IsInGroupAsync(string username, string groupName);
}
public class AuthResult
{
public bool Success { get; set; }
public string DisplayName { get; set; }
public string Email { get; set; }
public string ErrorMessage { get; set; }
}
```
### Production Implementation (LDAP)
```csharp
public class LdapAuthService : IAuthService
{
private readonly LdapOptions _options;
public LdapAuthService(IOptions<LdapOptions> options)
{
_options = options.Value;
}
public async Task<AuthResult> AuthenticateAsync(string username, string password)
{
using var connection = new LdapConnection(_options.Url);
try
{
connection.Bind(new NetworkCredential(username, password));
// Retrieve user details from directory
return new AuthResult { Success = true, DisplayName = "..." };
}
catch (LdapException)
{
return new AuthResult { Success = false, ErrorMessage = "Invalid credentials" };
}
}
}
```
### Development Implementation (Fake)
```csharp
public class FakeAuthService : IAuthService
{
private readonly AuthOptions _options;
public FakeAuthService(IOptions<AuthOptions> options)
{
_options = options.Value;
}
public Task<AuthResult> AuthenticateAsync(string username, string password)
{
// Accept any non-empty credentials in development
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
return Task.FromResult(new AuthResult
{
Success = false,
ErrorMessage = "Username and password required"
});
}
return Task.FromResult(new AuthResult
{
Success = true,
DisplayName = username,
Email = $"{username}@dev.local"
});
}
public Task<bool> IsInGroupAsync(string username, string groupName)
{
// Always return true in development
return Task.FromResult(true);
}
}
```
### Registration by Configuration
```csharp
// In Program.cs
var authOptions = builder.Configuration.GetSection("Auth").Get<AuthOptions>();
if (authOptions?.UseFakeAuth == true)
{
builder.Services.AddScoped<IAuthService, FakeAuthService>();
}
else
{
builder.Services.AddScoped<IAuthService, LdapAuthService>();
}
```
## Porting Strategy
The legacy Dapper queries in `LotFinderDB*.cs`, `JDE*.cs`, and `CMS*.cs` port with minimal changes:
- Update namespaces (`System.Data.SqlClient` to `Microsoft.Data.SqlClient`)
- Adapt CMS queries from Sybase to Oracle syntax
- Use async/await consistently
## Related Documentation
- [Solution Structure](./SolutionStructure.md)
- [Data Flow](./DataFlow.md)
- [Testing](./Testing.md)
+84
View File
@@ -0,0 +1,84 @@
# Data Flow
The system has two primary data flows: search execution and data synchronization.
## Search Flow
The search flow mirrors the legacy pattern, modernized for ASP.NET Core:
```
1. User submits search via Blazor UI
└─> POST /api/search (SearchCriteria JSON)
2. SearchController validates and stores in SQL Server
└─> Search record created with Status = "Queued"
└─> Returns SearchId to client
3. Client connects to SignalR StatusHub
└─> Subscribes to updates for their SearchId
4. SearchProcessorService (BackgroundService) polls
└─> Finds queued searches
└─> Executes query against local cache
└─> Generates Excel via ClosedXML
└─> Stores result in Search.Results (VARBINARY)
└─> Updates Status = "Complete"
5. StatusHub pushes update to client
└─> Client shows "Complete" status
6. User clicks download
└─> GET /api/search/{id}/download
└─> Returns Excel file stream
```
## Search Status States
| Status | Description |
|--------|-------------|
| `Queued` | Search submitted, waiting for processing |
| `Processing` | Background service is executing the search |
| `Generating` | Query complete, generating Excel file |
| `Complete` | Excel ready for download |
| `Failed` | Error occurred during processing |
## Data Sync Flow
The `DataSyncService` runs on a schedule to keep the local SQL Server cache current:
```
DataSyncService runs on schedule:
├── Mass refresh: Full reload (weekly or manual trigger)
├── Daily refresh: Last 24-48 hours of changes
└── Hourly refresh: Incremental updates
Each sync:
1. Determine tables to sync based on schedule
2. Query JDE/CMS Oracle for changes since last sync
3. Bulk insert/update to SQL Server cache
4. Update DataUpdate table with timestamp
```
## Sync Schedules
| Schedule | Frequency | Scope |
|----------|-----------|-------|
| Mass | Weekly (Sunday 2 AM) or manual | Full reload of all cached tables |
| Daily | Daily (3 AM) | Changes from last 48 hours |
| Hourly | Every hour | Incremental changes since last sync |
The schedules are configured via cron expressions in `appsettings.json` and parsed using the Cronos library.
## Database Connections
| Database | Purpose | Driver |
|----------|---------|--------|
| SQL Server | Local cache, search storage | Microsoft.Data.SqlClient |
| JDE Oracle | Enterprise work order data | Oracle.ManagedDataAccess.Core |
| CMS Oracle | Enterprise CMS data (migrated from Sybase) | Oracle.ManagedDataAccess.Core |
## Related Documentation
- [Host Project](./HostProject.md)
- [Core Project](./CoreProject.md)
- [Configuration](./Configuration.md)
+202
View File
@@ -0,0 +1,202 @@
# Database
The application uses SQL Server for the local cache database. Schema is managed using DbUp, with versioned SQL scripts embedded in the application.
## DbUp Overview
DbUp is a .NET library for deploying changes to SQL Server databases. It tracks which scripts have been executed in a `SchemaVersions` table and runs new scripts in alphabetical order.
Key benefits:
- Schema defined as code (versioned SQL scripts)
- Automatic migration on startup
- Idempotent - safe to run multiple times
- Simple, well-tested library
## Project Structure
```
JdeScoping.Database/
├── JdeScoping.Database.csproj
├── DatabaseMigrator.cs # Entry point for migrations
└── Scripts/
├── 001_CreateSearchTable.sql
├── 002_CreateDataUpdateTable.sql
├── 003_CreateWorkOrderTables.sql
├── 004_CreateLotTables.sql
├── 005_CreateReferenceTables.sql
└── ...
```
## Script Naming Convention
Scripts are named with a numeric prefix for ordering:
```
NNN_DescriptiveName.sql
```
- `NNN`: Zero-padded number (001, 002, etc.)
- `DescriptiveName`: Brief description of what the script does
- Scripts run in alphabetical order (numeric prefix ensures correct order)
## DatabaseMigrator Implementation
```csharp
using DbUp;
using Microsoft.Extensions.Configuration;
namespace JdeScoping.Database;
public class DatabaseMigrator
{
private readonly string _connectionString;
public DatabaseMigrator(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("SqlServer")
?? throw new InvalidOperationException("SqlServer connection string not configured");
}
public DatabaseUpgradeResult Migrate()
{
EnsureDatabase.For.SqlDatabase(_connectionString);
var upgrader = DeployChanges.To
.SqlDatabase(_connectionString)
.WithScriptsEmbeddedInAssembly(typeof(DatabaseMigrator).Assembly)
.WithTransaction()
.LogToConsole()
.Build();
return upgrader.PerformUpgrade();
}
}
```
## Embedding Scripts as Resources
Scripts are embedded in the assembly by configuring the project file:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Scripts\*.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="dbup-sqlserver" Version="5.*" />
</ItemGroup>
</Project>
```
## Running Migrations on Startup
Migrations run early in application startup, before other services are configured:
```csharp
// In Program.cs
var builder = WebApplication.CreateBuilder(args);
// Run database migrations first
var migrator = new DatabaseMigrator(builder.Configuration);
var result = migrator.Migrate();
if (!result.Successful)
{
Console.WriteLine($"Database migration failed: {result.Error}");
return 1;
}
// Continue with normal startup...
builder.Host.UseWindowsService();
```
## Core Tables
The scoping tool cache database includes these primary tables:
| Table | Purpose |
|-------|---------|
| `Search` | User search requests, status, and results (Excel as VARBINARY) |
| `DataUpdate` | Tracks last sync timestamp per data type |
| `WorkOrder_Curr` | Current work orders from JDE |
| `WorkOrder_Hist` | Historical work orders from JDE |
| `LotUsage_Curr` | Current lot usage from CMS |
| `LotUsage_Hist` | Historical lot usage from CMS |
| `Lot` | Lot reference data |
| `Item` | Item master reference data |
| `WorkCenter` | Work center reference data |
| `JdeUser` | Operator reference data |
| `ProfitCenter` | Profit center reference data |
| `SchemaVersions` | DbUp tracking table (auto-created) |
## Example Migration Scripts
### 001_CreateSearchTable.sql
```sql
CREATE TABLE [dbo].[Search] (
[Id] INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
[UserId] NVARCHAR(50) NOT NULL,
[UserDisplayName] NVARCHAR(100) NULL,
[Criteria] NVARCHAR(MAX) NOT NULL,
[Status] INT NOT NULL DEFAULT 0,
[CreatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[StartedAt] DATETIME2 NULL,
[CompletedAt] DATETIME2 NULL,
[ResultCount] INT NULL,
[Results] VARBINARY(MAX) NULL,
[ErrorMessage] NVARCHAR(MAX) NULL
);
CREATE INDEX [IX_Search_Status] ON [dbo].[Search] ([Status]);
CREATE INDEX [IX_Search_UserId] ON [dbo].[Search] ([UserId]);
```
### 002_CreateDataUpdateTable.sql
```sql
CREATE TABLE [dbo].[DataUpdate] (
[Id] INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
[TableName] NVARCHAR(100) NOT NULL,
[UpdateType] NVARCHAR(20) NOT NULL,
[LastUpdated] DATETIME2 NOT NULL,
[RecordCount] INT NULL,
[Status] NVARCHAR(20) NOT NULL DEFAULT 'Completed'
);
CREATE UNIQUE INDEX [IX_DataUpdate_TableName_Type]
ON [dbo].[DataUpdate] ([TableName], [UpdateType]);
```
## Development vs Production
The same migration scripts run in all environments. For development with file-based data sources, the cache tables are still created but populated from JSON/CSV files instead of Oracle.
## Adding New Migrations
1. Create a new SQL file with the next number prefix
2. Write idempotent SQL (use `IF NOT EXISTS` where appropriate)
3. Build and run - DbUp picks up new embedded scripts automatically
```sql
-- Example: 006_AddNewColumn.sql
IF NOT EXISTS (
SELECT 1 FROM sys.columns
WHERE object_id = OBJECT_ID('dbo.Search') AND name = 'Priority'
)
BEGIN
ALTER TABLE [dbo].[Search] ADD [Priority] INT NOT NULL DEFAULT 0;
END
```
## Related Documentation
- [Overview](./Overview.md)
- [Solution Structure](./SolutionStructure.md)
- [Configuration](./Configuration.md)
- [Data Flow](./DataFlow.md)
@@ -0,0 +1,95 @@
# Package Dependencies
All packages are free with MIT, Apache, or similar permissive licenses.
## JdeScoping.Host
| Package | Purpose | License |
|---------|---------|---------|
| `Microsoft.Extensions.Hosting.WindowsServices` | Windows Service support | MIT |
| `Microsoft.AspNetCore.SignalR` | Real-time updates | MIT |
## JdeScoping.Client
| Package | Purpose | License |
|---------|---------|---------|
| `Microsoft.AspNetCore.Components.WebAssembly` | Blazor WASM runtime | MIT |
| `Microsoft.AspNetCore.Components.WebAssembly.DevServer` | Dev server (dev only) | MIT |
| `Microsoft.AspNetCore.SignalR.Client` | SignalR client | MIT |
| `Radzen.Blazor` | UI components | MIT (free tier) |
## JdeScoping.Core
| Package | Purpose | License |
|---------|---------|---------|
| `Dapper` | Micro-ORM | Apache 2.0 |
| `Microsoft.Data.SqlClient` | SQL Server driver | MIT |
| `Oracle.ManagedDataAccess.Core` | Oracle driver | Oracle Free Use |
| `ClosedXML` | Excel generation | MIT |
| `System.DirectoryServices.Protocols` | LDAP authentication | MIT |
| `Cronos` | Cron expression parsing | MIT |
| `Microsoft.Extensions.Options` | Options pattern | MIT |
| `Microsoft.Extensions.Configuration.Abstractions` | Configuration abstractions | MIT |
## JdeScoping.Database
| Package | Purpose | License |
|---------|---------|---------|
| `dbup-sqlserver` | SQL Server database migrations | MIT |
## JdeScoping.Tests
| Package | Purpose | License |
|---------|---------|---------|
| `xunit` | Test framework | Apache 2.0 |
| `xunit.runner.visualstudio` | VS test runner | Apache 2.0 |
| `Microsoft.NET.Test.Sdk` | Test SDK | MIT |
| `Shouldly` | Assertions | BSD |
| `NSubstitute` | Mocking | BSD |
| `Microsoft.AspNetCore.Mvc.Testing` | Integration tests | MIT |
## Packages Explicitly Avoided
| Package | Reason |
|---------|--------|
| `FluentAssertions` | Commercial license since v6 |
| `EPPlus` (v5+) | Commercial license since v5 |
| `Kendo UI` | Commercial license, replaced by Radzen |
## Version Considerations
- Target **.NET 10** (LTS when released, currently .NET 9 is latest)
- Use latest stable versions of all packages
- `Oracle.ManagedDataAccess.Core` v3.x for .NET 6+ support
- `Radzen.Blazor` v5.x for .NET 8+ Blazor features
## Package Installation
```bash
# Host project
dotnet add src/JdeScoping.Host package Microsoft.Extensions.Hosting.WindowsServices
# Client project
dotnet add src/JdeScoping.Client package Radzen.Blazor
dotnet add src/JdeScoping.Client package Microsoft.AspNetCore.SignalR.Client
# Core project
dotnet add src/JdeScoping.Core package Dapper
dotnet add src/JdeScoping.Core package Microsoft.Data.SqlClient
dotnet add src/JdeScoping.Core package Oracle.ManagedDataAccess.Core
dotnet add src/JdeScoping.Core package ClosedXML
dotnet add src/JdeScoping.Core package Cronos
# Test project
dotnet add tests/JdeScoping.Tests package xunit
dotnet add tests/JdeScoping.Tests package Shouldly
dotnet add tests/JdeScoping.Tests package NSubstitute
# Database project
dotnet add src/JdeScoping.Database package dbup-sqlserver
```
## Related Documentation
- [Overview](./Overview.md)
- [Testing](./Testing.md)
+101
View File
@@ -0,0 +1,101 @@
# Host Project
The `JdeScoping.Host` project is the main entry point - an ASP.NET Core application that runs as a Windows Service.
## Program.cs Configuration
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWindowsService(); // Run as Windows Service
// ASP.NET Core services
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
// Background services
builder.Services.AddHostedService<SearchProcessorService>();
builder.Services.AddHostedService<DataSyncService>();
// Core dependencies (from JdeScoping.Core)
builder.Services.AddScoped<ISearchRepository, SearchRepository>();
builder.Services.AddScoped<ISearchService, SearchService>();
builder.Services.AddScoped<IExcelExportService, ExcelExportService>();
// Data source registration (file-based for dev, Oracle for prod)
var dataSourceOptions = builder.Configuration
.GetSection("DataSource").Get<DataSourceOptions>();
if (dataSourceOptions?.UseFileDataSource == true)
{
builder.Services.AddScoped<IJdeDataSource, JdeFileDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsFileDataSource>();
}
else
{
builder.Services.AddScoped<IJdeDataSource, JdeOracleDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsOracleDataSource>();
}
// Auth registration (fake for dev, LDAP for prod)
var authOptions = builder.Configuration
.GetSection("Auth").Get<AuthOptions>();
if (authOptions?.UseFakeAuth == true)
{
builder.Services.AddScoped<IAuthService, FakeAuthService>();
}
else
{
builder.Services.AddScoped<IAuthService, LdapAuthService>();
}
// Configuration
builder.Services.Configure<LdapOptions>(builder.Configuration.GetSection("Ldap"));
builder.Services.Configure<DataSyncOptions>(builder.Configuration.GetSection("DataSync"));
builder.Services.Configure<DataSourceOptions>(builder.Configuration.GetSection("DataSource"));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Auth"));
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapHub<StatusHub>("/hubs/status");
app.MapFallbackToFile("index.html");
app.Run();
```
## Controllers
| Controller | Purpose |
|------------|---------|
| `SearchController` | Submit search, get results, download Excel |
| `LookupController` | Autocomplete APIs for items, work centers, operators |
| `AuthController` | Login/logout against LDAP |
## Hubs
| Hub | Purpose |
|-----|---------|
| `StatusHub` | Pushes search status updates to connected clients |
## Background Services
| Service | Purpose |
|---------|---------|
| `SearchProcessorService` | Polls for queued searches, executes them, generates Excel |
| `DataSyncService` | Runs on schedule, syncs JDE/CMS data to local cache |
Background services use `IServiceScopeFactory` to create scopes for database access, avoiding scoped-in-singleton issues.
## Related Documentation
- [Solution Structure](./SolutionStructure.md)
- [Data Flow](./DataFlow.md)
- [Configuration](./Configuration.md)
+57
View File
@@ -0,0 +1,57 @@
# Architecture Overview
The JDE Scoping Tool is a manufacturing/ERP search application that caches data from JDE (Oracle) and CMS (Oracle) enterprise systems into SQL Server, allowing users to create complex searches and export results to Excel.
## Key Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Target framework | .NET 10 | Modern LTS, consolidation from .NET Framework 4.8 |
| Deployment | Self-hosted Kestrel as Windows Service | Simple, no IIS dependency |
| UI | Blazor WebAssembly + Radzen | Modern SPA, free component library |
| Database access | Dapper | Preserve existing queries, minimal changes |
| Oracle driver | Oracle.ManagedDataAccess.Core | Both JDE and CMS now on Oracle |
| Data sources | Interface + prod/dev implementations | Development uses file exports, production uses Oracle |
| Authentication | Interface + prod/dev implementations | Development uses fake auth, production uses LDAP |
| Real-time | ASP.NET Core SignalR | Push search status updates |
| Excel | ClosedXML | Free MIT license (replaces EPPlus) |
| Testing | xUnit + Shouldly + NSubstitute | Free, readable assertions |
| Config | appsettings.json + env vars | Standard, secrets via environment |
| Database migrations | DbUp | Schema defined in application, versioned SQL scripts |
## High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Windows Service Host │
│ ┌───────────────┐ ┌───────────────┐ ┌─────────────────┐ │
│ │ Blazor WASM │ │ REST API │ │ SignalR Hub │ │
│ │ Client │ │ Controllers │ │ (StatusHub) │ │
│ └───────────────┘ └───────────────┘ └─────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Background Services │ │
│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ SearchProcessor │ │ DataSyncService │ │ │
│ │ └─────────────────┘ └─────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ SQL Server │ │ JDE Oracle │ │ CMS Oracle │
│ (Local Cache)│ │ (Enterprise) │ │ (Enterprise) │
└───────────────┘ └───────────────┘ └───────────────┘
```
## Related Documentation
- [Solution Structure](./SolutionStructure.md)
- [Host Project](./HostProject.md)
- [Blazor Client](./BlazorClient.md)
- [Core Project](./CoreProject.md)
- [Database](./Database.md)
- [Data Flow](./DataFlow.md)
- [Configuration](./Configuration.md)
- [Testing](./Testing.md)
- [Dependencies](./Dependencies.md)
@@ -0,0 +1,71 @@
# Solution Structure
The solution uses a minimal project structure with four projects in a single deployable.
## Project Layout
```
NEW/
├── JdeScoping.sln
├── src/
│ ├── JdeScoping.Host/ # ASP.NET Core host + Blazor server
│ │ ├── Program.cs # Entry point, service configuration
│ │ ├── Controllers/ # API endpoints
│ │ ├── Hubs/ # SignalR hubs
│ │ ├── BackgroundServices/ # Search processor, data sync
│ │ └── wwwroot/ # Blazor WASM published output
│ │
│ ├── JdeScoping.Client/ # Blazor WebAssembly UI
│ │ ├── Pages/ # Razor pages
│ │ ├── Components/ # Reusable Radzen components
│ │ ├── Services/ # HTTP + SignalR clients
│ │ └── wwwroot/ # Static assets
│ │
│ ├── JdeScoping.Core/ # Shared business logic
│ │ ├── Models/ # Domain models (WorkOrder, Search, etc.)
│ │ ├── Interfaces/ # Repository/service contracts
│ │ ├── Repositories/ # Dapper data access
│ │ └── Services/ # Business logic
│ │
│ └── JdeScoping.Database/ # Database schema migrations
│ ├── Scripts/ # Versioned SQL scripts
│ │ ├── 001_CreateSearchTable.sql
│ │ ├── 002_CreateDataUpdateTable.sql
│ │ └── ...
│ └── DatabaseMigrator.cs # DbUp runner
└── tests/
└── JdeScoping.Tests/ # xUnit + Shouldly tests
├── Unit/
└── Integration/
```
## Project Responsibilities
### JdeScoping.Host
The deployable Windows Service. Hosts the Blazor WASM client, REST API, SignalR hub, and background services. References `JdeScoping.Core` for business logic.
### JdeScoping.Client
The Blazor WebAssembly UI. Compiled and published into Host's `wwwroot` folder. Uses Radzen Blazor for components. No direct database access - communicates via HTTP and SignalR.
### JdeScoping.Core
Business logic and data access. Contains domain models, repository interfaces and implementations, and services. No ASP.NET dependencies - fully testable in isolation.
### JdeScoping.Database
Database schema management using DbUp. Contains versioned SQL scripts that are embedded as resources and executed in order on application startup. Scripts are idempotent - DbUp tracks which scripts have run in a `SchemaVersions` table.
### JdeScoping.Tests
Unit and integration tests using xUnit, Shouldly for assertions, and NSubstitute for mocking.
## Related Documentation
- [Overview](./Overview.md)
- [Host Project](./HostProject.md)
- [Blazor Client](./BlazorClient.md)
- [Core Project](./CoreProject.md)
- [Database](./Database.md)
+181
View File
@@ -0,0 +1,181 @@
# Testing Strategy
The test project uses xUnit for the framework, Shouldly for assertions, and NSubstitute for mocking.
## Project Structure
```
JdeScoping.Tests/
├── Unit/
│ ├── Services/
│ │ ├── SearchServiceTests.cs
│ │ ├── ExcelExportServiceTests.cs
│ │ └── DataSyncOrchestratorTests.cs
│ ├── Repositories/
│ │ └── SearchRepositoryTests.cs
│ └── Models/
│ └── SearchCriteriaTests.cs
└── Integration/
├── ApiTests/
│ ├── SearchControllerTests.cs
│ └── LookupControllerTests.cs
└── RepositoryTests/
└── JdeRepositoryTests.cs
```
## Unit Tests
Unit tests mock dependencies and test business logic in isolation:
```csharp
public class SearchServiceTests
{
[Fact]
public async Task ExecuteSearch_WithValidCriteria_ReturnsResults()
{
// Arrange
var mockRepo = Substitute.For<ISearchRepository>();
mockRepo.GetWorkOrdersAsync(Arg.Any<SearchCriteria>())
.Returns(new List<WorkOrder> { new WorkOrder { Number = "WO123" } });
var service = new SearchService(mockRepo);
var criteria = new SearchCriteria { ItemNumber = "ABC123" };
// Act
var results = await service.ExecuteAsync(criteria);
// Assert
results.Count.ShouldBeGreaterThan(0);
results.First().Number.ShouldBe("WO123");
}
[Fact]
public async Task ExecuteSearch_WithInvalidCriteria_ThrowsValidationException()
{
// Arrange
var mockRepo = Substitute.For<ISearchRepository>();
var service = new SearchService(mockRepo);
var criteria = new SearchCriteria(); // Empty criteria
// Act & Assert
await Should.ThrowAsync<ValidationException>(
() => service.ExecuteAsync(criteria));
}
}
```
## Shouldly Assertions
Shouldly provides readable assertion syntax without FluentAssertions licensing:
```csharp
// Value assertions
result.ShouldBe(expected);
result.ShouldNotBeNull();
result.ShouldBeGreaterThan(0);
// Collection assertions
list.ShouldContain(item);
list.ShouldBeEmpty();
list.Count.ShouldBe(5);
// String assertions
text.ShouldStartWith("Error");
text.ShouldContain("expected");
// Exception assertions
Should.Throw<ArgumentException>(() => service.Process(null));
await Should.ThrowAsync<InvalidOperationException>(() => service.ProcessAsync());
```
## NSubstitute Mocking
NSubstitute provides a simple API for creating test doubles:
```csharp
// Create substitute
var mockRepo = Substitute.For<ISearchRepository>();
// Configure returns
mockRepo.GetByIdAsync(123).Returns(new Search { Id = 123 });
mockRepo.GetByIdAsync(Arg.Any<int>()).Returns(x => new Search { Id = (int)x[0] });
// Verify calls
await mockRepo.Received().CreateAsync(Arg.Is<Search>(s => s.Status == "Queued"));
await mockRepo.DidNotReceive().DeleteAsync(Arg.Any<int>());
```
## Integration Tests
Integration tests use `WebApplicationFactory<Program>` for API tests:
```csharp
public class SearchControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public SearchControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task SubmitSearch_ReturnsSearchId()
{
// Arrange
var criteria = new SearchCriteria { ItemNumber = "TEST123" };
var content = new StringContent(
JsonSerializer.Serialize(criteria),
Encoding.UTF8,
"application/json");
// Act
var response = await _client.PostAsync("/api/search", content);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<SearchResult>();
result.SearchId.ShouldBeGreaterThan(0);
}
}
```
## Database Integration Tests
Repository integration tests run against a local SQL Server instance:
```csharp
public class SearchRepositoryIntegrationTests : IDisposable
{
private readonly string _connectionString;
public SearchRepositoryIntegrationTests()
{
_connectionString = "Server=localhost;Database=LotFinder_Test;...";
// Setup test database
}
[Fact]
public async Task CreateAndRetrieve_RoundTrips()
{
var repo = new SearchRepository(_connectionString);
var search = new Search { UserId = "testuser", Status = "Queued" };
var id = await repo.CreateAsync(search);
var retrieved = await repo.GetByIdAsync(id);
retrieved.ShouldNotBeNull();
retrieved.UserId.ShouldBe("testuser");
}
public void Dispose()
{
// Cleanup test data
}
}
```
## Related Documentation
- [Core Project](./CoreProject.md)
- [Dependencies](./Dependencies.md)
+249
View File
@@ -0,0 +1,249 @@
# Component Map
This document maps source code locations to their corresponding documentation folders. Use this to determine where documentation should live.
## Project Structure
This is a migration project with two main code areas:
```
JdeScopingTool/
├── OLD/ # Legacy .NET Framework 4.8 (read-only reference)
├── NEW/ # New .NET 10 solution (build target)
├── SPECS/ # OpenSpec specifications
└── DOCUMENTATION/ # Project documentation
```
## Source to Documentation Mapping
### Legacy Code (OLD/)
| Source Path | Documentation Folder |
|-------------|---------------------|
| `OLD/WebInterface/` | `LegacyReference/WebInterface/` |
| `OLD/WebInterface/Controllers/` | `LegacyReference/WebInterface/Controllers.md` |
| `OLD/WebInterface/Hubs/` | `LegacyReference/WebInterface/SignalR.md` |
| `OLD/WorkerService/` | `LegacyReference/WorkerService/` |
| `OLD/WorkerService/Process/` | `LegacyReference/WorkerService/Processing.md` |
| `OLD/DataModel/` (Commons) | `LegacyReference/DataModel/` |
| `OLD/DataModel/Process/JDE*.cs` | `DataSync/JDE.md` |
| `OLD/DataModel/Process/CMS*.cs` | `DataSync/CMS.md` |
| `OLD/DataModel/Process/LotFinderDB*.cs` | `Database/LocalCache.md` |
| `OLD/DataModel/Models/` | `Database/Entities.md` |
| `OLD/Database/` | `Database/Schema.md` |
### New Code (NEW/)
| Source Path | Documentation Folder |
|-------------|---------------------|
| `NEW/src/` | Component-specific folders |
| `NEW/src/Web/Controllers/` | `API/Endpoints.md` |
| `NEW/src/Web/Hubs/` | `API/SignalR.md` |
| `NEW/src/Web/Client/` (Blazor) | `WebClient/` |
| `NEW/src/Services/` | `BackgroundServices/` |
| `NEW/src/Core/Models/` | `Database/Entities.md` |
| `NEW/src/Core/Interfaces/` | Document in implementing component |
| `NEW/src/Infrastructure/` | Component-specific folders |
| `NEW/appsettings*.json` | `Configuration/` |
| `NEW/tests/` | Document in corresponding component |
## Documentation Folder Structure
```
DOCUMENTATION/
├── Instructions/ # This folder - documentation guidelines
├── GettingStarted/ # Onboarding, prerequisites, architecture overview
├── LegacyReference/ # Documentation of the OLD codebase for migration reference
│ ├── WebInterface/ # ASP.NET MVC web application
│ ├── WorkerService/ # Windows service (Topshelf)
│ └── DataModel/ # Shared library (Commons.csproj)
├── Search/ # Search functionality
├── DataSync/ # JDE/CMS data synchronization
├── Database/ # SQL Server cache, entities, schema
├── API/ # REST endpoints, SignalR
├── WebClient/ # Blazor WASM UI
├── Export/ # Excel generation
├── Authentication/ # LDAP authentication
├── BackgroundServices/ # Background processing
├── Configuration/ # appsettings, connection strings
└── Operations/ # Deployment, monitoring
```
## Component Details
### Search/
Documents the search functionality.
**Source paths (Legacy):**
- `OLD/DataModel/Models/Search*.cs` - Search models
- `OLD/DataModel/ViewModels/Search*.cs` - Search view models
- `OLD/WebInterface/Controllers/SearchController.cs` - Search API
- `OLD/WorkerService/Process/WorkProcessor.cs` - Search execution
**Source paths (New):**
- `NEW/src/Core/Models/Search*.cs` - Search models
- `NEW/src/Web/Controllers/SearchController.cs` - Search API
- `NEW/src/Services/SearchProcessor.cs` - Search execution
**Typical files:**
- `Overview.md` - Search system architecture
- `Criteria.md` - Search criteria model and options
- `Execution.md` - How searches are processed
- `Results.md` - Result storage and retrieval
### DataSync/
Documents data synchronization from enterprise systems.
**Source paths (Legacy):**
- `OLD/DataModel/Process/JDE*.cs` - JDE Oracle queries
- `OLD/DataModel/Process/CMS*.cs` - CMS Sybase queries
- `OLD/WorkerService/Process/UpdateProcessor.cs` - Sync orchestration
- `OLD/WorkerService/dsconfig/*.json` - Data source configs
**Source paths (New):**
- `NEW/src/Infrastructure/DataSync/` - Data sync services
- `NEW/src/Infrastructure/DataSync/JDE/` - JDE adapter
- `NEW/src/Infrastructure/DataSync/CMS/` - CMS adapter
**Typical files:**
- `Overview.md` - Data sync architecture
- `JDE.md` - JD Edwards (Oracle) integration
- `CMS.md` - CMS (Sybase) integration
- `Scheduling.md` - Mass/daily/hourly sync schedules
- `Configuration.md` - Data source configuration
### Database/
Documents the SQL Server cache database.
**Source paths (Legacy):**
- `OLD/DataModel/Process/LotFinderDB*.cs` - SQL Server access
- `OLD/DataModel/Models/` - Entity definitions
- `OLD/Database/` - SQL Server database project
**Source paths (New):**
- `NEW/src/Infrastructure/Persistence/` - EF Core implementation
- `NEW/src/Core/Models/` - Entity definitions
**Typical files:**
- `Overview.md` - Database architecture
- `Entities.md` - Entity documentation
- `LocalCache.md` - Cached data tables
- `Schema.md` - Database schema reference
### API/
Documents the REST API and real-time communication.
**Source paths (Legacy):**
- `OLD/WebInterface/Controllers/` - MVC controllers
- `OLD/WebInterface/Hubs/StatusHub.cs` - SignalR hub
**Source paths (New):**
- `NEW/src/Web/Controllers/` - API controllers
- `NEW/src/Web/Hubs/` - SignalR hubs
**Typical files:**
- `Overview.md` - API architecture
- `Endpoints.md` - Endpoint reference
- `SignalR.md` - Real-time status updates
- `Authentication.md` - JWT/LDAP auth
### WebClient/
Documents the Blazor WASM frontend.
**Source paths (New):**
- `NEW/src/Web/Client/Pages/` - Page components
- `NEW/src/Web/Client/Components/` - Reusable components
- `NEW/src/Web/Client/Services/` - Client services
**Typical files:**
- `Overview.md` - Frontend architecture
- `Components.md` - Component documentation
- `SearchUI.md` - Search interface
- `State.md` - State management
### Export/
Documents Excel export functionality.
**Source paths (Legacy):**
- `OLD/WorkerService/Process/ExcelWriter.cs` - EPPlus generation
- `OLD/WorkerService/Templates/QueryTemplate.cs` - Query templates
**Source paths (New):**
- `NEW/src/Services/ExcelExporter.cs` - Excel generation
**Typical files:**
- `Overview.md` - Export architecture
- `Excel.md` - Excel generation details
- `Templates.md` - Export templates
### Authentication/
Documents LDAP authentication.
**Source paths (Legacy):**
- `OLD/WebInterface/Controllers/AccountController.cs` - Login/logout
- `OLD/DataModel/Config.cs` - LDAP configuration
**Source paths (New):**
- `NEW/src/Infrastructure/Authentication/` - Auth services
**Typical files:**
- `Overview.md` - Authentication architecture
- `LDAP.md` - LDAP integration details
- `Configuration.md` - Auth configuration
### BackgroundServices/
Documents background processing.
**Source paths (Legacy):**
- `OLD/WorkerService/` - Topshelf Windows service
- `OLD/WorkerService/Process/WorkProcessor.cs` - Main work loop
**Source paths (New):**
- `NEW/src/Services/` - BackgroundService implementations
**Typical files:**
- `Overview.md` - Background service architecture
- `SearchProcessor.md` - Search queue processing
- `DataSyncService.md` - Data synchronization service
### LegacyReference/
Documents the legacy codebase for migration reference.
**Source paths:**
- All files under `OLD/`
**Typical files:**
- `WebInterface/Overview.md` - ASP.NET MVC structure
- `WorkerService/Overview.md` - Topshelf service structure
- `DataModel/Overview.md` - Commons library structure
- `MigrationNotes.md` - Patterns to preserve/change
## Ambiguous Cases
When code spans multiple components, use these guidelines:
| Code Type | Document In |
|-----------|-------------|
| SignalR hubs | `API/SignalR.md` |
| SignalR clients | `WebClient/SignalR.md` |
| Shared DTOs | Component that "owns" the concept |
| Cross-cutting services | Most relevant component |
| Legacy patterns | `LegacyReference/` with cross-references |
## Adding New Components
When adding a new system component:
1. Create a new folder under `DOCUMENTATION/`
2. Add at minimum `Overview.md`
3. Update this mapping table
4. Update [GeneratingDocs.md](./GeneratingDocs.md) if new patterns emerge
@@ -0,0 +1,145 @@
# Generating Documentation
This guide defines how to create new documentation for the JDE Scoping Tool migration project. Follow these instructions when documenting new features, components, or systems.
## Document Types
Each component folder should contain these standard files:
| File | Purpose |
|------|---------|
| `Overview.md` | What the component does, key concepts, architecture diagrams |
| `Development.md` | How to add/modify features, patterns to follow, best practices |
| `Configuration.md` | All configurable options with defaults and examples |
| `Troubleshooting.md` | Common issues, error messages, debugging steps |
Create additional topic-specific files as needed (e.g., `Search/Criteria.md`, `DataSync/JDE.md`, `Export/Excel.md`).
## Generation Process
### Step 1: Identify Scope
Determine what you're documenting:
- Which component folder does this belong to? (See [ComponentMap.md](./ComponentMap.md))
- Is this a new document or an addition to an existing one?
- What source files contain the implementation?
### Step 2: Read Source Code
Before writing any documentation:
1. Read the relevant source files thoroughly
2. Understand the current implementation, not assumptions
3. Identify key classes, methods, and patterns
4. Note any configuration options or environment variables
5. Look for existing code comments that explain "why"
### Step 3: Check Existing Documentation
Avoid duplication:
1. Search `DOCUMENTATION/` for related content
2. If similar content exists, update it rather than creating new
3. Cross-reference related docs rather than repeating information
### Step 4: Write Documentation
Structure your document following the [StyleGuide.md](./StyleGuide.md):
```markdown
# Component/Feature Name
Brief 1-2 sentence description of what this is and why it exists.
## Key Concepts
Explain the important ideas a developer needs to understand.
## Usage
### Basic Example
```csharp
// Code snippet from actual source
```
### Common Patterns
Describe typical usage patterns with code examples.
## Configuration
| Option | Default | Description |
|--------|---------|-------------|
| `OptionName` | `value` | What it does |
## Related Documentation
- [Related Topic](../OtherComponent/RelatedTopic.md)
```
### Step 5: Verify Accuracy
Before finalizing:
1. Confirm all code snippets match actual source code
2. Verify file paths and class names are correct
3. Test any commands or configuration examples
4. Ensure cross-references point to existing files
## Required Sections
Every documentation file must include:
1. **Title and Purpose** - H1 heading with 1-2 sentence description
2. **Key Concepts** - If the topic requires background understanding
3. **Code Examples** - Embedded snippets from actual codebase
4. **Configuration** - If the component has configurable options
5. **Related Documentation** - Links to related topics
## Code Snippet Guidelines
### Do
- Copy snippets from actual source files
- Include enough context (class name, method signature)
- Show typical 5-25 line examples
- Specify the language in code blocks
```csharp
public class SearchProcessor : BackgroundService
{
private readonly ISearchRepository _searchRepository;
public SearchProcessor(ISearchRepository searchRepository)
{
_searchRepository = searchRepository;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await ProcessQueuedSearchesAsync(stoppingToken);
}
}
```
### Don't
- Invent example code that doesn't exist in the codebase
- Include 100+ line dumps without explanation
- Use pseudocode when real code is available
- Omit the language specifier on code blocks
## File Naming
- Use `PascalCase.md` for all documentation files
- Match the concept being documented: `Actors.md`, `Migrations.md`, `SignalR.md`
- For multi-word topics: `HealthChecks.md`, `StateMachines.md`
- Avoid abbreviations unless universally understood: `API.md` is fine, `TIA.md` is not
## Creating New Component Folders
When documenting a new system component:
1. Create the folder under `DOCUMENTATION/`
2. Add at minimum `Overview.md`
3. Add other standard files as content warrants
4. Update [ComponentMap.md](./ComponentMap.md) with the new mapping
5. Add cross-references from related documentation
+282
View File
@@ -0,0 +1,282 @@
# Documentation Style Guide
This guide defines writing conventions and formatting rules for all JDE Scoping Tool documentation.
## Tone and Voice
### Be Technical and Direct
Write for developers who are familiar with .NET. Don't explain basic concepts like dependency injection or async/await unless they're used in an unusual way.
**Good:**
> The `SearchProcessor` executes queued searches against the local cache and generates Excel results.
**Avoid:**
> The SearchProcessor is a really powerful component that helps manage all your searches efficiently!
### Explain "Why" Not Just "What"
Document the reasoning behind patterns and decisions, not just the mechanics.
**Good:**
> Data sync uses a retry pattern with exponential backoff because JDE connections can be temporarily unavailable during peak ERP usage.
**Avoid:**
> Data sync uses retry with backoff.
### Use Present Tense
Describe what the code does, not what it will do.
**Good:**
> The actor validates the message before processing.
**Avoid:**
> The actor will validate the message before processing.
### No Marketing Language
This is internal technical documentation. Avoid superlatives and promotional language.
**Avoid:** "powerful", "robust", "cutting-edge", "seamless", "blazing fast"
## Formatting Rules
### File Names
Use `PascalCase.md` for all documentation files:
- `Overview.md`
- `HealthChecks.md`
- `StateMachines.md`
- `SignalR.md`
### Headings
- **H1 (`#`):** Document title only, Title Case
- **H2 (`##`):** Major sections, Title Case
- **H3 (`###`):** Subsections, Sentence case
- **H4+ (`####`):** Rarely needed, Sentence case
```markdown
# Actor Health Checks
## Configuration Options
### Setting the timeout
#### Default values
```
### Code Blocks
Always specify the language:
````markdown
```csharp
public class MyActor : ReceiveActor { }
```
```json
{
"Setting": "value"
}
```
```bash
dotnet build
```
````
Supported languages: `csharp`, `json`, `bash`, `xml`, `sql`, `yaml`, `html`, `css`, `javascript`
### Code Snippets
**Length:** 5-25 lines is typical. Shorter for simple concepts, longer for complete examples.
**Context:** Include enough to understand where the code lives:
```csharp
// Good - shows class context
public class SearchProcessor : BackgroundService
{
public SearchProcessor(ISearchRepository repository)
{
_repository = repository;
}
}
// Avoid - orphaned snippet
_repository = repository;
```
**Accuracy:** Only use code that exists in the codebase. Never invent examples.
### Lists
Use bullet points for unordered items:
```markdown
- First item
- Second item
- Third item
```
Use numbers for sequential steps:
```markdown
1. Do this first
2. Then do this
3. Finally do this
```
### Tables
Use tables for structured reference information:
```markdown
| Option | Default | Description |
|--------|---------|-------------|
| `Timeout` | `5000` | Milliseconds to wait |
| `RetryCount` | `3` | Number of retry attempts |
```
### Inline Code
Use backticks for:
- Class names: `SearchProcessor`
- Method names: `ProcessQueuedSearches()`
- File names: `appsettings.json`
- Configuration keys: `JdeScoping:ConnectionStrings`
- Command-line commands: `dotnet build`
### Links
Use relative paths for internal documentation:
```markdown
[See the Search guide](../Search/Overview.md)
[Configuration options](./Configuration.md)
```
Use descriptive link text:
```markdown
<!-- Good -->
See the [Data Sync Configuration](../DataSync/Configuration.md) documentation.
<!-- Avoid -->
See [here](../DataSync/Configuration.md) for more.
```
## Structure Conventions
### Document Opening
Every document starts with:
1. H1 title
2. 1-2 sentence description of purpose
```markdown
# Search Processor
The search processor handles queued searches, executing queries against the local cache and generating Excel results.
```
### Section Organization
Organize content from general to specific:
1. Overview/introduction
2. Key concepts (if needed)
3. Basic usage
4. Advanced usage
5. Configuration
6. Troubleshooting
7. Related documentation
### Code Example Placement
Place code examples immediately after the concept they illustrate:
```markdown
## Search Execution
Searches are executed against the local cache using Dapper:
```csharp
var results = await connection.QueryAsync<WorkOrder>(query, parameters);
```
Each search returns a collection of matching records...
```
### Related Documentation Section
End each document with links to related topics:
```markdown
## Related Documentation
- [Search Criteria](./Criteria.md)
- [Excel Export](../Export/Excel.md)
- [Configuration](../Configuration/Search.md)
```
## Naming Conventions
### Match Code Exactly
Use the exact names from source code:
- `SearchProcessor` not "Search Processor"
- `WorkOrder` not "Work Order"
- `ISearchRepository` not "search repository interface"
### Acronyms
Spell out on first use, then use acronym:
> JD Edwards (JDE) is an Oracle ERP system. JDE stores work order and lot data...
Common acronyms that don't need expansion:
- API
- JSON
- SQL
- HTTP/HTTPS
- REST
- JWT
- UI
### File Paths
Use forward slashes and backticks:
- `NEW/src/Services/SearchProcessor.cs`
- `appsettings.json`
- `DOCUMENTATION/Search/Overview.md`
## What to Avoid
### Don't Document the Obvious
```markdown
<!-- Avoid -->
## Constructor
The constructor creates a new instance of the class.
<!-- Better - only document if there's something notable -->
## Constructor
The constructor accepts an `IActorRef` for the gateway actor, which must be resolved before actor creation.
```
### Don't Duplicate Source Code Comments
If code has good comments, reference the file rather than copying:
> See `SearchProcessor.cs` lines 45-60 for the search execution logic.
### Don't Include Temporary Information
Avoid dates, version numbers, or "coming soon" notes that will become stale.
### Don't Over-Explain .NET Basics
Assume readers know:
- Dependency injection
- async/await
- LINQ
- Entity Framework basics
- ASP.NET Core middleware pipeline
+156
View File
@@ -0,0 +1,156 @@
# Updating Documentation
This guide defines when and how to update existing documentation. Documentation should always reflect the current codebase state.
## Update Triggers
When these code changes occur, update the corresponding documentation:
| Code Change | Update These Docs |
|-------------|-------------------|
| New API endpoint | `API/Endpoints.md`, update `API/Overview.md` |
| API endpoint changed | Corresponding endpoint documentation |
| New entity added | `Database/Entities.md` |
| Search criteria modified | `Search/Criteria.md` |
| New data sync source | `DataSync/` relevant file (JDE, CMS, etc.) |
| Data sync logic changed | Corresponding data sync documentation |
| Excel export modified | `Export/Excel.md` |
| New Blazor component | `WebClient/Components.md` |
| SignalR hub changed | `API/SignalR.md` |
| Background service modified | `BackgroundServices/` relevant file |
| Config option added | Component's `Configuration.md` |
| Config option removed | Remove from docs |
| Authentication changed | `Authentication/LDAP.md` |
| Deployment config changed | `Operations/Deployment.md` |
| appsettings changed | `Configuration/` relevant file |
## Update Process
### Step 1: Identify Affected Documentation
Use [ComponentMap.md](./ComponentMap.md) to determine which docs need updating:
1. Identify the source files that changed
2. Map them to documentation folders
3. List all potentially affected documentation files
### Step 2: Read Current Documentation
Before making changes:
1. Read the entire document you're updating
2. Understand the existing structure and flow
3. Identify sections that need modification
### Step 3: Make Targeted Updates
Keep changes minimal and focused:
- Only modify sections affected by the code change
- Don't rewrite unaffected sections
- Preserve existing explanations that are still accurate
- Maintain consistent style with surrounding content
### Step 4: Update Code Snippets
If the code change affects documented examples:
1. Locate all code snippets that reference the changed code
2. Update snippets to match the new implementation
3. Verify the updated snippets compile/work correctly
### Step 5: Update Cross-References
If the change creates or removes relationships:
1. Add links to newly related documentation
2. Remove links to deleted content
3. Update link text if document titles changed
### Step 6: Add Verification Comment
At the bottom of updated documents, add or update:
```markdown
<!-- Last verified against codebase: YYYY-MM-DD -->
```
## Deletion Handling
### When Code Is Removed
1. **Remove corresponding documentation sections**
- Delete paragraphs describing removed features
- Remove code snippets that no longer apply
- Update "Key Concepts" if concepts no longer exist
2. **Update cross-references**
- Search all docs for links to removed content
- Either remove the link or redirect to replacement content
- Update any "See also" or "Related" sections
3. **Handle complete feature removal**
- If an entire file's subject is removed, delete the file
- Update any index or overview docs that referenced it
- Check navigation/table of contents if applicable
### When Code Is Renamed
1. Update all documentation references to use new names
2. Update code snippets with new class/method/variable names
3. Rename documentation files if they match the old name
4. Update all cross-reference links to renamed files
## Batch Updates
When making large-scale code changes:
1. **List all affected documentation** before starting
2. **Update systematically** - one document at a time
3. **Verify consistency** across updated documents
4. **Check cross-references** after all updates complete
## Common Update Scenarios
### Adding a New Search Field
1. Add entry to `Search/Criteria.md` with:
- Field name and data type
- Which data source it queries (JDE, CMS, local cache)
- How it affects search results
- Example usage in search criteria
2. Update `Search/Overview.md` if the field affects search architecture
3. Update `Database/Entities.md` if new cache tables are involved
### Adding a New API Endpoint
1. Add entry to `API/Endpoints.md` with:
- HTTP method and route
- Request/response format
- Authentication requirements
- Example request/response
2. Update `API/Overview.md` if endpoint represents new functionality
### Adding a New Data Sync Source
1. Add entry to `DataSync/` folder with:
- Source system details (connection type, schema)
- Tables/data being synced
- Sync schedule (mass/daily/hourly)
- Code snippet showing query patterns
2. Update `DataSync/Overview.md` with the new source
3. Update `Database/Entities.md` with any new cache tables
### Changing Configuration Options
1. Update the relevant `Configuration.md` file
2. If option affects multiple components, update each
3. Update any deployment documentation if defaults changed
4. Update troubleshooting docs if option helps debug issues
## What Not to Update
Avoid unnecessary changes:
- Don't reformat documentation that wasn't affected
- Don't update examples if they still work correctly
- Don't add new content unrelated to the code change
- Don't change writing style in unaffected sections
+473
View File
@@ -0,0 +1,473 @@
# Search Creation Page - Functionality Analysis
This document provides a comprehensive analysis of the legacy search creation page (`OLD/WebInterface/Views/Search/Create.cshtml`) for migration to the new .NET 10 Blazor application.
## Overview
The search creation page allows users to create complex manufacturing/ERP searches by combining various filter criteria. It uses Kendo UI for data binding and widgets, jQuery FileUpload for Excel file handling, and SignalR for real-time status updates.
---
## Page Structure
### Header Section
- **Title**: "Search"
- **Submit Button**: Triggers validation and saves the search
### Search Details Panel
| Field | Type | Behavior |
|-------|------|----------|
| Search Type | Dropdown | **Required**. Selects from 16 predefined filter combinations. Controls which filter panels are visible. |
| Name | Text input | **Required**. User-friendly name for the search. |
| Submitted At | Read-only text | Displays when search was submitted (formatted: `MM/dd/yyyy hh:mm:ss tt`) |
| Started At | Read-only text | Displays when processing started |
| Completed At | Read-only text | Displays when processing completed |
| User | Read-only text | Username of search creator (auto-populated) |
| Status | Read-only text | Current status with color coding (red background for Error status) |
| Download Results | Button | Visible only when `Status === 'Ended'`. Downloads Excel results. |
### Read-Only Mode
When a search has been submitted (`Status !== 'New'`):
- Submit button is hidden
- All inputs are disabled
- Template upload/download/clear buttons are hidden
- A warning notice is displayed with a **Copy** button to duplicate the search
---
## Valid Search Type Combinations
The system enforces 16 predefined filter combinations defined in `OLD/WebInterface/Scripts/model/models.js`:
| ID | Name | Timespan | Work Order | Item Number | Profit Center | Work Center | Component Lot | Operator | Item/Op/MIS | Extract MIS |
|----|------|----------|------------|-------------|---------------|-------------|---------------|----------|-------------|-------------|
| 10 | Work Order | | x | | | | | | | |
| 20 | Component Lot | | | | | | x | | | |
| 30 | Time Span + Profit Center | x | | | x | | | | | |
| 40 | Time Span + Work Center | x | | | | x | | | | |
| 50 | Time Span + Operator | x | | | | | | x | | |
| 60 | Time Span + Profit Center + Item Number | x | | x | x | | | | | |
| 70 | Time Span + Profit Center + Item/Operation/MIS | x | | | x | | | | x | |
| 80 | Time Span + Profit Center + Work Order + Item/Operation/MIS | x | x | | x | | | | x | |
| 90 | Time Span + Profit Center + Extract MIS | x | | | x | | | | | x |
| 100 | Time Span + Work Center + Item Number | x | | x | | x | | | | |
| 110 | Time Span + Work Center + Extract MIS | x | | | | x | | | | x |
| 120 | Time Span + Work Center + Item/Operation/MIS | x | | | | x | | | x | |
| 130 | Time Span + Work Center + Work Order + Item/Operation/MIS | x | x | | | x | | | x | |
| 140 | Time Span + Item Number | x | | x | | | | | | |
| 150 | Time Span + Work Center + Operator | x | | | | x | | x | | |
| 160 | Time Span + Profit Center + Operator | x | | | x | | | x | | |
---
## Filter Panels
### 1. Time Span Filter
**Panel Header**: "Filter by timespan"
| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| Min Date | Kendo DatePicker | **Required** when filter is active. Must be valid date. | Min: Nov 1, 2002. Max: Today or Max Date if set. |
| Max Date | Kendo DatePicker | **Required** when filter is active. Must be valid date. | Min: Nov 1, 2002 or Min Date if set. Max: Today. |
**Interactions**:
- Min/Max pickers constrain each other (selecting min date sets min of max picker, and vice versa)
- Custom validation prevents invalid date text
---
### 2. Work Order Filter
**Panel Header**: "Filter by work order"
**Data Display**: Kendo Grid showing:
- Work Order Number
- Item Number (looked up from database)
**File Operations**:
| Button | Action | Endpoint |
|--------|--------|----------|
| Download Template | Downloads Excel with current data | `POST FileIO/DownloadWorkOrders``GET FileIO/DownloadWorkOrders?key` |
| Upload Data | Uploads Excel file, validates work orders against DB | `POST FileIO/UploadWorkOrders` |
| Clear Data | Clears grid after confirmation dialog | Local action |
**Upload Format**: Excel file with column "Work Order Number" (starting row 2)
**Validation**: At least one work order required when filter is active
---
### 3. Item Number Filter
**Panel Header**: "Filter by item number"
**Input Method**: Kendo ComboBox with server-side autocomplete
- Minimum 3 characters to trigger search
- Filter: "contains"
- Template displays: Item Number | Description
- Endpoint: `GET Lookup/FindItem?itemNumber=...`
**Data Display**: Kendo Grid showing:
- Item Number
- Description
- Delete action button
**Actions**:
| Button | Action |
|--------|--------|
| Add to filter | Adds selected item from combobox to grid |
| Delete (per row) | Removes item from grid |
| Clear Data | Clears grid after confirmation |
| Download Template | Downloads Excel with current items |
| Upload Data | Uploads Excel file of item numbers |
**File Operations**:
| Button | Endpoint |
|--------|----------|
| Download Template | `POST FileIO/DownloadPartNumbers``GET FileIO/DownloadPartNumbers?key` |
| Upload Data | `POST FileIO/UploadPartNumbers` |
**Upload Format**: Excel file with column "Item Number" (starting row 2)
**Validation**: At least one item required when filter is active
---
### 4. Profit Center Filter
**Panel Header**: "Filter by profit center"
**Input Method**: Kendo ComboBox with server-side autocomplete
- Minimum 3 characters to trigger search
- Filter: "contains"
- Template displays: Code | Description
- Endpoint: `GET Lookup/FindProfitCenter?profitCenter=...`
**Data Display**: Kendo Grid showing:
- Code (Profit Center)
- Description
- Delete action button
**Actions**:
| Button | Action |
|--------|--------|
| Add to filter | Adds selected profit center from combobox to grid |
| Delete (per row) | Removes profit center from grid |
| Clear Data | Clears grid after confirmation |
**No file upload/download** for this filter.
**Validation**: At least one profit center required when filter is active
---
### 5. Work Center Filter
**Panel Header**: "Filter by work center"
**Input Method**: Kendo ComboBox with server-side autocomplete
- Minimum 3 characters to trigger search
- Filter: "contains"
- Template displays: Code | Description
- Endpoint: `GET Lookup/FindWorkCenter?workCenter=...`
**Data Display**: Kendo Grid showing:
- Code (Work Center)
- Description
- Delete action button
**Actions**:
| Button | Action |
|--------|--------|
| Add to filter | Adds selected work center from combobox to grid |
| Delete (per row) | Removes work center from grid |
| Clear Data | Clears grid after confirmation |
**No file upload/download** for this filter.
**Validation**: At least one work center required when filter is active
---
### 6. Component Lot Filter
**Panel Header**: "Filter by component lot"
**Data Display**: Kendo Grid showing:
- Lot Number
- Item Number
**File Operations**:
| Button | Action | Endpoint |
|--------|--------|----------|
| Download Template | Downloads Excel with current data | `POST FileIO/DownloadComponentLots``GET FileIO/DownloadComponentLots?key` |
| Upload Data | Uploads Excel file, validates lots against DB | `POST FileIO/UploadComponentLots` |
| Clear Data | Clears grid after confirmation dialog | Local action |
**Upload Format**: Excel file with columns "Component Lot Number", "Component Item Number" (starting row 2)
**Validation**: At least one component lot required when filter is active
---
### 7. Operator Filter
**Panel Header**: "Filter by operator"
**Input Method**: Kendo ComboBox with server-side autocomplete
- Minimum 3 characters to trigger search
- Filter: "contains"
- Template displays: Address Number | User ID | Full Name
- Endpoint: `GET Lookup/FindOperator?operatorName=...`
**Data Display**: Kendo Grid showing:
- Address Number
- User ID
- Full Name
- Delete action button
**Actions**:
| Button | Action |
|--------|--------|
| Add to filter | Adds selected operator from combobox to grid |
| Delete (per row) | Removes operator from grid |
| Clear Data | Clears grid after confirmation |
**No file upload/download** for this filter.
**Validation**: At least one operator required when filter is active
---
### 8. Item/Operation/MIS Filter
**Panel Header**: "Filter By Item/Operation/MIS"
**Data Display**: Kendo Grid showing:
- Item Number
- Operation Step Number
- MIS Number
- MIS Revision
**File Operations**:
| Button | Action | Endpoint |
|--------|--------|----------|
| Download Template | Downloads Excel with current data | `POST FileIO/DownloadPartOperations``GET FileIO/DownloadPartOperations?key` |
| Upload Data | Uploads Excel file (no DB validation) | `POST FileIO/UploadPartOperations` |
| Clear Data | Clears grid after confirmation dialog | Local action |
**Upload Format**: Excel file with columns "Item Number", "Operation Number", "MIS Number", "MIS Revision" (starting row 2)
**Note**: Operation numbers with decimals are truncated to integers during upload.
**Validation**: At least one entry required when filter is active
---
### 9. Extract MIS Data Option
**Panel Header**: "Extract MIS data"
**Display**: Read-only checkbox that is automatically checked when this search type is selected. Not user-editable.
---
## API Endpoints Summary
### Search Operations
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `Search/Create` | GET | Renders the search creation view |
| `Search/GetSearch?id=` | GET | Loads existing search or creates blank search |
| `Search/CopySearch?id=` | GET | Creates copy of existing search (resets status/timestamps) |
| `Search/Save` | POST | Saves search criteria, queues for processing |
| `Search/GetResults?id=` | GET | Downloads Excel results for completed search |
### Lookup/Autocomplete
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `Lookup/FindItem?itemNumber=` | GET | Searches items by number (contains filter) |
| `Lookup/FindProfitCenter?profitCenter=` | GET | Searches profit centers by code |
| `Lookup/FindWorkCenter?workCenter=` | GET | Searches work centers by code |
| `Lookup/FindOperator?operatorName=` | GET | Searches operators by name |
### File I/O
| Endpoint | Method | Purpose | Template File |
|----------|--------|---------|---------------|
| `FileIO/UploadWorkOrders` | POST | Upload work order Excel | - |
| `FileIO/DownloadWorkOrders` | POST/GET | Download work order template | `work_order_template.xlsx` |
| `FileIO/UploadPartNumbers` | POST | Upload item number Excel | - |
| `FileIO/DownloadPartNumbers` | POST/GET | Download item template | `item_number_template.xlsx` |
| `FileIO/UploadComponentLots` | POST | Upload component lot Excel | - |
| `FileIO/DownloadComponentLots` | POST/GET | Download component lot template | `component_lot_template.xlsx` |
| `FileIO/UploadPartOperations` | POST | Upload item/op/MIS Excel | - |
| `FileIO/DownloadPartOperations` | POST/GET | Download item/op/MIS template | `item_operations_mis_template.xlsx` |
---
## JavaScript Architecture
### Libraries Used
- **Kendo UI**: Observable viewModel, DataSource, Grid, ComboBox, DatePicker, DropDownList, Validator, Alert, Window
- **jQuery FileUpload**: Handles Excel file uploads with iframe transport
- **SignalR 2.2.1**: Real-time status updates from server
- **js-cookie**: Cookie handling (included but usage not prominent)
- **jQuery UI**: General UI utilities
### ViewModel Structure
The page uses a Kendo Observable viewModel with the following properties:
```javascript
{
// Search details
ID: null,
Name: null,
SubmitDT: null,
StartDT: null,
EndDT: null,
UserName: null,
Status: null,
StatusColor: function(), // Returns '#FF6347' for Error, '#eee' otherwise
// Filter flags (control panel visibility)
TimeSpan_FilterFlag: false,
LotNumbers_FilterFlag: false,
PartNumbers_FilterFlag: false,
ProfitCenters_FilterFlag: false,
WorkCenters_FilterFlag: false,
ComponentLotNumbers_FilterFlag: false,
OperatorIDs_FilterFlag: false,
PartOperations_FilterFlag: false,
ExtractMisData_FilterFlag: false,
// Filter data
MinimumDT: null,
MaximumDT: null,
LotNumbers: DataSource,
PartNumbers: DataSource,
ProfitCenters: DataSource,
WorkCenters: DataSource,
ComponentLotNumbers: DataSource,
OperatorIDs: DataSource,
PartOperations: DataSource,
// Combobox selection state
PartNumbers_AddItem: null,
ProfitCenters_AddItem: null,
WorkCenters_AddItem: null,
OperatorIDs_AddItem: null,
// UI state
IsReadOnly: true,
HasResults: false,
ValidCombinations: [...], // 16 valid search types
SearchType: null
}
```
### Key Functions
| Function | Purpose |
|----------|---------|
| `viewModel.setData(data)` | Populates viewModel from server response, determines search type from filter flags |
| `viewModel.SearchType_Change()` | Shows/hides filter panels based on selected search type |
| `submitSearch()` | Extracts form data, sends to server, handles timeout/errors |
| `loadSearchDetails(id)` | Loads existing search from server |
| `copySearchDetails(id)` | Loads search for copying (resets ID to 0) |
| `showConfirmationWindow(message)` | Displays Kendo confirmation dialog, returns Promise |
| `getParameterByName(name)` | Extracts URL query parameter |
---
## SignalR Integration
### Hub Connection
- Connects to `StatusHub` via `/signalr/hubs`
- Auto-reconnects after 5 seconds on disconnect
### Events
| Event | Purpose |
|-------|---------|
| `searchUpdate` | Receives status updates when search status changes. Updates SubmitDT, StartDT, EndDT, Status, HasResults. |
### 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 |
---
## Validation Rules
### Form-Level Validation (Kendo Validator)
1. **Search Type**: Required
2. **Name**: Required
3. **Date Pickers**: Must be valid dates when visible
### Filter-Level Validation (Submit Handler)
When a filter panel is active, its data collection must have at least one item:
| Filter | Validation Message |
|--------|-------------------|
| Work Orders | "At least one work order must be specified for the work order filter." |
| Item Numbers | "At least one item number must be specified for the item number filter." |
| Profit Centers | "At least one profit center must be specified for the profit center filter." |
| Work Centers | "At least one work center must be specified for the work center filter." |
| Component Lots | "At least one component lot must be specified for the component lot filter." |
| Operators | "At least one operator must be specified for the operator filter." |
| Part Operations | "At least one item/operation/MIS entry must be specified for the MIS data filter." |
### Confirmation Dialogs
Shown before:
- Submitting the search
- Clearing any filter data collection
---
## File Upload/Download Flow
### Upload Flow
1. User clicks hidden file input via styled label
2. jQuery FileUpload sends file to endpoint with `autoUpload: true`
3. Server parses Excel, optionally validates against database
4. Server returns `{ WasSuccessful: true/false, Data: [...], ErrorMessage: "..." }`
5. On success, viewModel DataSource is updated with returned data
6. On failure, `alert()` displays error message
### Download Flow
1. User clicks Download Template button
2. Current data is POSTed to server
3. Server generates Excel, caches it with GUID key (1 minute TTL)
4. Server returns cache key
5. Client appends hidden iframe with `src` pointing to GET endpoint with key
6. Browser downloads file via iframe
---
## Dead Code / Legacy References
- `CheckCamstar_Flag`: Referenced in submit payload (`Create.cshtml:1343`) but no corresponding viewModel property or UI element exists. Likely deprecated functionality.
---
## Migration Considerations
### UI Framework Changes
- Replace Kendo UI with MudBlazor or similar Blazor component library
- Replace Kendo Observable with Blazor component state
- Replace Kendo DataSource with standard .NET collections
- Replace Kendo Validator with Blazor EditForm validation
### File Handling
- Replace jQuery FileUpload with Blazor file upload (InputFile component)
- Keep EPPlus for Excel generation
- Consider streaming large files
### Real-Time Updates
- Replace legacy SignalR with ASP.NET Core SignalR
- Update hub connection patterns for Blazor
### API Structure
- Keep similar endpoint structure
- Update controllers for ASP.NET Core
- Consider REST API patterns with proper HTTP methods
### State Management
- Consider Fluxor or similar state management for complex form state
- Or use cascading parameters for simpler approach
+859
View File
@@ -0,0 +1,859 @@
# 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)
```razor
@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
```csharp
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:
```csharp
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
```razor
@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)
```razor
@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)
```razor
@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)
```razor
@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)
```razor
@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)
```razor
@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)
```razor
@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
```razor
@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
```csharp
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
```csharp
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)
```csharp
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
```csharp
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
```csharp
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:**
```javascript
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
```csharp
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
```csharp
@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
```razor
<EditForm Model="@ViewModel" OnValidSubmit="@OnValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<!-- Form content -->
</EditForm>
```
### SearchViewModel Validation Attributes
```csharp
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)
```csharp
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:
```csharp
@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
---
## Related Documentation
- [Architecture Overview](./Architecture/Overview.md)
- [Blazor Client](./Architecture/BlazorClient.md)
- [Dependencies](./Architecture/Dependencies.md)
- [Data Flow](./Architecture/DataFlow.md)