diff --git a/NEW/JdeScoping.slnx b/NEW/JdeScoping.slnx
index 3a8b38c..3963b6b 100644
--- a/NEW/JdeScoping.slnx
+++ b/NEW/JdeScoping.slnx
@@ -15,6 +15,7 @@
+
diff --git a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs
index 2a429f6..895e59c 100644
--- a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs
+++ b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs
@@ -42,22 +42,30 @@ public class RefreshStatusController : ApiControllerBase
// Get data updates filtered in SQL by date range (end of day for maxDT)
var updates = await _repository.GetDataUpdatesInRangeAsync(minDT, maxDT.Date.AddDays(1), ct);
- // Group by StartDt (rounded to minute) to aggregate multiple table updates into single rows
+ // Group by StartDt (rounded to minute) to aggregate multiple table updates into single rows.
+ // Within each run, deduplicate by TableName so each table appears once even when
+ // multiple UpdateTypes (Hourly/Daily/Mass) ran in the same minute.
var aggregated = updates
.GroupBy(u => new DateTime(u.StartDt.Year, u.StartDt.Month, u.StartDt.Day, u.StartDt.Hour, u.StartDt.Minute, 0))
- .Select(g => new DataUpdateDto
+ .Select(g =>
{
- StartDt = g.Key,
- EndDt = g.Max(u => u.EndDt),
- WasSuccessful = g.All(u => u.WasSuccessful),
- PassedCount = g.Count(u => u.WasSuccessful),
- FailedCount = g.Count(u => !u.WasSuccessful),
- Items = g.Select(u => new DataUpdateItemDto
+ var items = g.GroupBy(u => u.TableName)
+ .Select(tg => new DataUpdateItemDto
+ {
+ TableName = tg.Key,
+ WasSuccessful = tg.All(u => u.WasSuccessful),
+ NumberRecords = tg.Sum(u => Math.Max(0, u.NumberRecords))
+ }).OrderBy(i => i.TableName).ToList();
+
+ return new DataUpdateDto
{
- TableName = u.TableName,
- WasSuccessful = u.WasSuccessful,
- NumberRecords = u.NumberRecords
- }).OrderBy(i => i.TableName).ToList()
+ StartDt = g.Key,
+ EndDt = g.Max(u => u.EndDt),
+ WasSuccessful = items.All(i => i.WasSuccessful),
+ PassedCount = items.Count(i => i.WasSuccessful),
+ FailedCount = items.Count(i => !i.WasSuccessful),
+ Items = items
+ };
})
.OrderByDescending(d => d.StartDt)
.ToList();
diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs b/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs
index efeadd8..8c2e4af 100644
--- a/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs
+++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs
@@ -34,20 +34,15 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T
[Parameter]
public bool IsReadOnly { get; set; }
- ///
- /// The current search text.
- ///
- protected string SearchText { get; set; } = "";
-
///
/// The search results from the API.
///
protected List SearchResults { get; set; } = [];
///
- /// The currently selected item from search results.
+ /// The currently selected value from the dropdown.
///
- protected TItem? SelectedItem { get; set; }
+ protected TItem? SelectedValue { get; set; }
///
/// Reference to the data grid for explicit refresh.
@@ -93,13 +88,6 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T
/// The unique key value.
protected abstract object GetItemKey(TItem item);
- ///
- /// Gets the display text value for an item (used for matching in autocomplete).
- ///
- /// The item to get the display text for.
- /// The display text value.
- protected abstract string GetDisplayText(TItem item);
-
///
/// Handles the autocomplete search.
///
@@ -107,7 +95,7 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T
/// A task representing the asynchronous search operation.
protected async Task OnSearchAsync(LoadDataArgs args)
{
- if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3)
+ if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 2)
{
SearchResults = await SearchApiAsync(args.Filter);
}
@@ -117,40 +105,23 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T
}
}
- ///
- /// Handles selection from the autocomplete.
- ///
- /// The selected value from the autocomplete control.
- protected void OnItemSelected(object value)
- {
- if (value is string text && !string.IsNullOrEmpty(text))
- {
- SelectedItem = SearchResults.FirstOrDefault(i => GetDisplayText(i) == text);
- }
- else
- {
- SelectedItem = null;
- }
- }
-
///
/// Adds the selected item to the list.
///
protected async Task AddItemAsync()
{
- if (SelectedItem != null)
+ if (SelectedValue != null)
{
- var selectedKey = GetItemKey(SelectedItem);
+ var selectedKey = GetItemKey(SelectedValue);
var isDuplicate = Items.Any(i => GetItemKey(i).Equals(selectedKey));
if (!isDuplicate)
{
- Items.Add(SelectedItem);
+ Items.Add(SelectedValue);
await ItemsChanged.InvokeAsync(Items);
}
}
- SearchText = "";
- SelectedItem = null;
+ SelectedValue = null;
}
///
diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor b/NEW/src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
index 89336ca..b76b412 100644
--- a/NEW/src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
+++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
@@ -24,19 +24,17 @@
@if (!IsReadOnly)
{
-
-
-
-
-
-
-
-
-
-
+
}
@@ -72,9 +70,8 @@
[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;
- private string _searchText = "";
private List _searchResults = [];
- private ItemViewModel? _selectedItem;
+ private ItemViewModel? _selectedValue;
private bool _isUploading;
private RadzenDataGrid? _grid;
@@ -98,27 +95,14 @@
}
}
- private void OnItemSelected(object value)
- {
- if (value is string text && !string.IsNullOrEmpty(text))
- {
- _selectedItem = _searchResults.FirstOrDefault(i => i.ItemNumber == text);
- }
- else
- {
- _selectedItem = null;
- }
- }
-
private async Task AddItemAsync()
{
- if (_selectedItem != null && !Items.Any(i => i.ItemNumber == _selectedItem.ItemNumber))
+ if (_selectedValue != null && !Items.Any(i => i.ItemNumber == _selectedValue.ItemNumber))
{
- Items.Add(_selectedItem);
+ Items.Add(_selectedValue);
await ItemsChanged.InvokeAsync(Items);
}
- _searchText = "";
- _selectedItem = null;
+ _selectedValue = null;
}
private async Task DeleteItem(ItemViewModel item)
diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor b/NEW/src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
index 45dcb47..a95c8a8 100644
--- a/NEW/src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
+++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
@@ -15,19 +15,17 @@
@if (!IsReadOnly)
{
-
-
-
-
-
-
-
-
-
-
+
}
@@ -63,13 +61,12 @@
}
protected override string PanelTitle => "Filter by Operator";
- protected override string SearchPlaceholder => "Search operators (3+ chars)...";
+ protected override string SearchPlaceholder => "Search operators (2+ chars)...";
protected override string SearchFieldLabel => "Name";
protected override string AutocompleteTextProperty => "FullName";
protected override string ClearConfirmMessage => "Are you sure you want to clear all operators?";
protected override object GetItemKey(OperatorViewModel item) => item.UserId;
- protected override string GetDisplayText(OperatorViewModel item) => item.FullName;
protected override async Task> SearchApiAsync(string filter)
{
diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor b/NEW/src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
index c98e51d..6fff1bf 100644
--- a/NEW/src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
+++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
@@ -14,19 +14,17 @@
@if (!IsReadOnly)
{
-
-
-
-
-
-
-
-
-
-
+
}
@@ -61,13 +59,12 @@
}
protected override string PanelTitle => "Filter by Profit Center";
- protected override string SearchPlaceholder => "Search profit centers (3+ chars)...";
+ protected override string SearchPlaceholder => "Search profit centers (2+ chars)...";
protected override string SearchFieldLabel => "Profit Center";
protected override string AutocompleteTextProperty => "Code";
protected override string ClearConfirmMessage => "Are you sure you want to clear all profit centers?";
protected override object GetItemKey(ProfitCenterViewModel item) => item.Code;
- protected override string GetDisplayText(ProfitCenterViewModel item) => item.Code;
protected override async Task> SearchApiAsync(string filter)
{
diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor b/NEW/src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
index 458602f..d40fab4 100644
--- a/NEW/src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
+++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
@@ -14,19 +14,17 @@
@if (!IsReadOnly)
{
-
-
-
-
-
-
-
-
-
-
+
}
@@ -61,13 +59,12 @@
}
protected override string PanelTitle => "Filter by Work Center";
- protected override string SearchPlaceholder => "Search work centers (3+ chars)...";
+ protected override string SearchPlaceholder => "Search work centers (2+ chars)...";
protected override string SearchFieldLabel => "Work Center";
protected override string AutocompleteTextProperty => "Code";
protected override string ClearConfirmMessage => "Are you sure you want to clear all work centers?";
protected override object GetItemKey(WorkCenterViewModel item) => item.Code;
- protected override string GetDisplayText(WorkCenterViewModel item) => item.Code;
protected override async Task> SearchApiAsync(string filter)
{
diff --git a/NEW/src/JdeScoping.Client/Components/Search/SearchDetailsSection.razor b/NEW/src/JdeScoping.Client/Components/Search/SearchDetailsSection.razor
index bce9009..4135556 100644
--- a/NEW/src/JdeScoping.Client/Components/Search/SearchDetailsSection.razor
+++ b/NEW/src/JdeScoping.Client/Components/Search/SearchDetailsSection.razor
@@ -7,61 +7,55 @@
@namespace JdeScoping.Client.Components.Search
- Search Details
+
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
@if (Search.HasResults)
{
-
-
-
+
+
}
diff --git a/NEW/src/JdeScoping.Client/Pages/SearchEdit.razor b/NEW/src/JdeScoping.Client/Pages/SearchEdit.razor
index 5b85db7..e24dbca 100644
--- a/NEW/src/JdeScoping.Client/Pages/SearchEdit.razor
+++ b/NEW/src/JdeScoping.Client/Pages/SearchEdit.razor
@@ -29,7 +29,7 @@
Search
@if (!_search.IsReadOnly)
{
-
+
}
diff --git a/NEW/src/JdeScoping.Client/wwwroot/css/app.css b/NEW/src/JdeScoping.Client/wwwroot/css/app.css
index c928e77..f777f2c 100644
--- a/NEW/src/JdeScoping.Client/wwwroot/css/app.css
+++ b/NEW/src/JdeScoping.Client/wwwroot/css/app.css
@@ -296,6 +296,53 @@ code {
padding: 0;
}
+/* Legacy-style form labels - bold, above the field */
+.field-label {
+ font-weight: bold;
+ margin-bottom: 0.25rem;
+ font-size: 0.95rem;
+ color: #333;
+}
+
+/* Read-only input styling - grey background */
+.readonly-input {
+ background-color: #eee !important;
+ color: #555 !important;
+ cursor: default;
+}
+
+/* Search details card header bar */
+.search-details-header {
+ background-color: #f5f5f5;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid #ddd;
+ margin: -1.25rem -1.25rem 1rem -1.25rem;
+ font-size: 1rem;
+}
+
+/* Blue submit button (legacy style) */
+.btn-submit-blue {
+ background-color: #337ab7 !important;
+ border-color: #2e6da4 !important;
+ color: #fff !important;
+}
+
+.btn-submit-blue:hover {
+ background-color: #286090 !important;
+ border-color: #204d74 !important;
+}
+
+/* Filter panel input row - align Add button with input bottom */
+.filter-input-row {
+ display: flex;
+ align-items: flex-end;
+ gap: 0.5rem;
+}
+
+.filter-input-row .filter-input-col {
+ flex: 0 1 41.67%;
+}
+
/* RadzenUpload inline button style (no drop zone) */
.rz-upload-inline .rz-fileupload-buttonbar {
padding: 0;
diff --git a/NEW/src/Utils/JdeScoping.DevLoader/JdeScoping.DevLoader.csproj b/NEW/src/Utils/JdeScoping.DevLoader/JdeScoping.DevLoader.csproj
new file mode 100644
index 0000000..0138e47
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.DevLoader/JdeScoping.DevLoader.csproj
@@ -0,0 +1,22 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ jdescoping-devloader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NEW/src/Utils/JdeScoping.DevLoader/Program.cs b/NEW/src/Utils/JdeScoping.DevLoader/Program.cs
new file mode 100644
index 0000000..4e6e4f6
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.DevLoader/Program.cs
@@ -0,0 +1,88 @@
+using System.Diagnostics;
+using JdeScoping.DataSync.Dev;
+using JdeScoping.DataSync.Dev.Options;
+using JdeScoping.DataSync.Dev.Services;
+using JdeScoping.DevLoader;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Serilog;
+
+const string DefaultConnectionString =
+ "Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=Sc0ping@pp_Dev#2024;TrustServerCertificate=true;Encrypt=false";
+
+// --- Parse arguments ---
+string? cacheDir = null;
+string connectionString = DefaultConnectionString;
+
+for (var i = 0; i < args.Length; i++)
+{
+ switch (args[i])
+ {
+ case "--cache-dir" when i + 1 < args.Length:
+ cacheDir = args[++i];
+ break;
+ case "--connection-string" when i + 1 < args.Length:
+ connectionString = args[++i];
+ break;
+ }
+}
+
+if (string.IsNullOrWhiteSpace(cacheDir))
+{
+ Console.Error.WriteLine("Usage: jdescoping-devloader --cache-dir [--connection-string ]");
+ return 1;
+}
+
+// --- Set up Serilog ---
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .CreateLogger();
+
+using var loggerFactory = LoggerFactory.Create(builder =>
+ builder.AddSerilog(Log.Logger, dispose: false));
+
+var logger = loggerFactory.CreateLogger();
+var pipelineLogger = loggerFactory.CreateLogger();
+
+// --- Build components ---
+var connectionFactory = new SimpleDbConnectionFactory(connectionString);
+var options = Options.Create(new DevPipelineOptions());
+var pipelineFactory = new DevEtlPipelineFactory(connectionFactory, options, pipelineLogger);
+var registry = new DevEtlRegistry(pipelineFactory, cacheDir, logger);
+
+// --- Run ---
+var tables = registry.GetAvailableTables().ToList();
+Log.Information("Found {Count} tables to load from {Dir}", tables.Count, cacheDir);
+Log.Information("Tables: {Tables}", string.Join(", ", tables));
+
+var sw = Stopwatch.StartNew();
+var results = await registry.RunAllParallelAsync(maxDegreeOfParallelism: 4);
+sw.Stop();
+
+// --- Report ---
+Log.Information("=== Dev ETL Complete ({Elapsed:g}) ===", sw.Elapsed);
+
+var succeeded = 0;
+var failed = 0;
+long totalRows = 0;
+
+foreach (var r in results.OrderBy(r => r.Success ? 0 : 1))
+{
+ var tableName = r.Steps.FirstOrDefault()?.StepName?.Replace("_Dev", "") ?? "Unknown";
+ if (r.Success)
+ {
+ succeeded++;
+ totalRows += r.TotalRows;
+ Log.Information(" OK {Table,-30} {Rows,10:N0} rows ({Elapsed:g})", tableName, r.TotalRows, r.Elapsed);
+ }
+ else
+ {
+ failed++;
+ Log.Error(" FAIL {Table,-30} {Error}", tableName, r.Error?.Message);
+ }
+}
+
+Log.Information("Summary: {Succeeded} succeeded, {Failed} failed, {TotalRows:N0} total rows", succeeded, failed, totalRows);
+
+return failed > 0 ? 1 : 0;
diff --git a/NEW/src/Utils/JdeScoping.DevLoader/SimpleDbConnectionFactory.cs b/NEW/src/Utils/JdeScoping.DevLoader/SimpleDbConnectionFactory.cs
new file mode 100644
index 0000000..c491b42
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.DevLoader/SimpleDbConnectionFactory.cs
@@ -0,0 +1,39 @@
+using JdeScoping.DataAccess.Interfaces;
+using Microsoft.Data.SqlClient;
+using Oracle.ManagedDataAccess.Client;
+
+namespace JdeScoping.DevLoader;
+
+///
+/// Minimal connection factory for the dev loader.
+/// Only supports SQL Server (LotFinder cache); Oracle methods throw NotSupportedException.
+///
+public class SimpleDbConnectionFactory : IDbConnectionFactory
+{
+ private readonly string _sqlConnectionString;
+
+ public SimpleDbConnectionFactory(string sqlConnectionString)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(sqlConnectionString);
+ _sqlConnectionString = sqlConnectionString;
+ }
+
+ public async Task CreateLotFinderConnectionAsync(CancellationToken ct = default)
+ {
+ var conn = new SqlConnection(_sqlConnectionString);
+ await conn.OpenAsync(ct);
+ return conn;
+ }
+
+ public Task CreateJdeConnectionAsync(CancellationToken ct = default)
+ => throw new NotSupportedException("Oracle JDE connections are not available in the dev loader.");
+
+ public Task CreateJdeStageConnectionAsync(CancellationToken ct = default)
+ => throw new NotSupportedException("Oracle JDE Stage connections are not available in the dev loader.");
+
+ public Task CreateCmsConnectionAsync(CancellationToken ct = default)
+ => throw new NotSupportedException("Oracle CMS connections are not available in the dev loader.");
+
+ public Task CreateGiwConnectionAsync(CancellationToken ct = default)
+ => throw new NotSupportedException("Oracle GIW connections are not available in the dev loader.");
+}