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 +
+ 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."); +}