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:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user