diff --git a/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs b/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs index 82a59c7..1a896c3 100644 --- a/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs @@ -73,6 +73,7 @@ public class ManualSyncController : ApiControllerBase public ActionResult> GetPipelines() { var pipelines = _pipelineRegistry.GetEnabledPipelines() + .OrderBy(p => p.Name) .Select(p => new PipelineInfoViewModel { Name = p.Name, diff --git a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs index ec6a476..2a429f6 100644 --- a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs @@ -39,33 +39,25 @@ public class RefreshStatusController : ApiControllerBase [FromQuery] DateTime maxDT, CancellationToken ct) { - // Get raw data updates from repository - var updates = await _repository.GetLastDataUpdatesAsync(ct); - - // Filter by date range - var filtered = updates - .Where(u => u.StartDt >= minDT && u.StartDt <= maxDT.AddDays(1)) - .ToList(); + // 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 - var aggregated = filtered + 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 { StartDt = g.Key, EndDt = g.Max(u => u.EndDt), WasSuccessful = g.All(u => u.WasSuccessful), - BranchRecords = (int)(g.FirstOrDefault(u => u.TableName == "Branch")?.NumberRecords ?? 0), - ProfitCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "ProfitCenter")?.NumberRecords ?? 0), - WorkCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "WorkCenter")?.NumberRecords ?? 0), - OrgHierarchyRecords = (int)(g.FirstOrDefault(u => u.TableName == "OrgHierarchy")?.NumberRecords ?? 0), - StatusCodeRecords = (int)(g.FirstOrDefault(u => u.TableName == "StatusCode")?.NumberRecords ?? 0), - UserRecords = (int)(g.FirstOrDefault(u => u.TableName == "JdeUser")?.NumberRecords ?? 0), - ItemRecords = (int)(g.FirstOrDefault(u => u.TableName == "Item")?.NumberRecords ?? 0), - LotRecords = (int)(g.FirstOrDefault(u => u.TableName == "Lot")?.NumberRecords ?? 0), - WorkOrderRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrder_"))?.NumberRecords ?? 0), - WorkOrderStepRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrderStep"))?.NumberRecords ?? 0), - WorkOrderComponentRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrderComponent"))?.NumberRecords ?? 0) + PassedCount = g.Count(u => u.WasSuccessful), + FailedCount = g.Count(u => !u.WasSuccessful), + Items = g.Select(u => new DataUpdateItemDto + { + TableName = u.TableName, + WasSuccessful = u.WasSuccessful, + NumberRecords = u.NumberRecords + }).OrderBy(i => i.TableName).ToList() }) .OrderByDescending(d => d.StartDt) .ToList(); diff --git a/NEW/src/JdeScoping.Client/Components/RefreshStatus/RefreshStatusDetailDialog.razor b/NEW/src/JdeScoping.Client/Components/RefreshStatus/RefreshStatusDetailDialog.razor new file mode 100644 index 0000000..88403b5 --- /dev/null +++ b/NEW/src/JdeScoping.Client/Components/RefreshStatus/RefreshStatusDetailDialog.razor @@ -0,0 +1,38 @@ +@* + RefreshStatusDetailDialog.razor - Per-table detail popup for a sync run. + + Displays a grid of individual table sync results (table name, record count, pass/fail). +*@ + + + @SyncRun.StartDt.ToString("MM/dd/yyyy hh:mm tt") — @(SyncRun.EndDt?.ToString("hh:mm tt") ?? "In Progress") + + + + + + + + + + + + +
+ +
+ +@code { + [Parameter] public DataUpdateDto SyncRun { get; set; } = default!; + + [Inject] private DialogService DialogService { get; set; } = default!; +} diff --git a/NEW/src/JdeScoping.Client/Pages/RefreshStatus.razor b/NEW/src/JdeScoping.Client/Pages/RefreshStatus.razor index 303bb9c..a29438a 100644 --- a/NEW/src/JdeScoping.Client/Pages/RefreshStatus.razor +++ b/NEW/src/JdeScoping.Client/Pages/RefreshStatus.razor @@ -2,11 +2,12 @@ RefreshStatus.razor - Data cache refresh status dashboard. Shows the status of JDE/CMS data synchronization jobs (hourly, daily, mass). - Allows filtering by date range and entity name. + Allows filtering by date range. Click a row to see per-table detail. *@ @page "/refresh-status" @attribute [Authorize] @inject IRefreshStatusService RefreshStatusService +@inject DialogService DialogService Cache Refresh Status - JDE Scoping Tool @@ -38,38 +39,30 @@ else { + PagerHorizontalAlign="HorizontalAlign.Center" AllowColumnResize="true" Style="text-align: center;" + SelectionMode="DataGridSelectionMode.Single" RowSelect="@OnRowSelect"> - + - + - - - - - - - - - - - - + + + @@ -96,7 +89,6 @@ else try { _results = await RefreshStatusService.GetRefreshStatusAsync(_minDt, _maxDt); - // Sort by StartDT descending _results = _results.OrderByDescending(r => r.StartDt).ToList(); } finally @@ -104,4 +96,12 @@ else _isLoading = false; } } + + private async Task OnRowSelect(DataUpdateDto row) + { + await DialogService.OpenAsync( + "Sync Run Detail", + new Dictionary { { "SyncRun", row } }, + new DialogOptions { Width = "600px", Resizable = true, Draggable = true }); + } } diff --git a/NEW/src/JdeScoping.Core/Interfaces/ILotFinderRepository.DataSync.cs b/NEW/src/JdeScoping.Core/Interfaces/ILotFinderRepository.DataSync.cs index 4d3a369..007f992 100644 --- a/NEW/src/JdeScoping.Core/Interfaces/ILotFinderRepository.DataSync.cs +++ b/NEW/src/JdeScoping.Core/Interfaces/ILotFinderRepository.DataSync.cs @@ -13,4 +13,13 @@ public partial interface ILotFinderRepository /// Cancellation token. /// Latest data updates. Task> GetLastDataUpdatesAsync(CancellationToken ct = default); + + /// + /// Gets all data update records within a date range. + /// + /// Start of range (inclusive). + /// End of range (inclusive, end of day). + /// Cancellation token. + /// Data updates within the range. + Task> GetDataUpdatesInRangeAsync(DateTime minDt, DateTime maxDt, CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs index 5bb6d2e..1c99cd9 100644 --- a/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs +++ b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs @@ -2,38 +2,25 @@ namespace JdeScoping.Core.Models.Infrastructure; /// /// DTO for data refresh/sync status display. -/// Aggregates record counts from multiple table updates into a single row. +/// Summarises a single sync run with pass/fail counts and per-table detail. /// public class DataUpdateDto { /// The start time of the data update. public DateTime StartDt { get; set; } + /// The end time of the data update. public DateTime? EndDt { get; set; } - /// The number of branch records updated. - public int BranchRecords { get; set; } - /// The number of profit center records updated. - public int ProfitCenterRecords { get; set; } - /// The number of work center records updated. - public int WorkCenterRecords { get; set; } - /// The number of organizational hierarchy records updated. - public int OrgHierarchyRecords { get; set; } - /// The number of status code records updated. - public int StatusCodeRecords { get; set; } - /// The number of user records updated. - public int UserRecords { get; set; } - /// The number of item records updated. - public int ItemRecords { get; set; } - /// The number of lot records updated. - public int LotRecords { get; set; } - /// The number of work order records updated. - public int WorkOrderRecords { get; set; } - /// The number of work order step records updated. - public int WorkOrderStepRecords { get; set; } - /// The number of work order component records updated. - public int WorkOrderComponentRecords { get; set; } - - /// Whether the data update was successful. + /// Whether the data update was successful overall. public bool WasSuccessful { get; set; } + + /// Number of table syncs that succeeded. + public int PassedCount { get; set; } + + /// Number of table syncs that failed. + public int FailedCount { get; set; } + + /// Per-table detail for this sync run. + public List Items { get; set; } = []; } diff --git a/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateItemDto.cs b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateItemDto.cs new file mode 100644 index 0000000..8eed00f --- /dev/null +++ b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateItemDto.cs @@ -0,0 +1,16 @@ +namespace JdeScoping.Core.Models.Infrastructure; + +/// +/// Per-table detail within a data sync run. +/// +public class DataUpdateItemDto +{ + /// The cache table that was synced. + public string TableName { get; set; } = string.Empty; + + /// Whether this table sync succeeded. + public bool WasSuccessful { get; set; } + + /// Number of records synced for this table. + public long NumberRecords { get; set; } +} diff --git a/NEW/src/JdeScoping.DataAccess/Queries/LotFinderQueries.DataSync.cs b/NEW/src/JdeScoping.DataAccess/Queries/LotFinderQueries.DataSync.cs index 07614d5..de520e7 100644 --- a/NEW/src/JdeScoping.DataAccess/Queries/LotFinderQueries.DataSync.cs +++ b/NEW/src/JdeScoping.DataAccess/Queries/LotFinderQueries.DataSync.cs @@ -24,4 +24,21 @@ public static partial class LotFinderQueries cte.NumberRecords FROM DU_CTE cte WHERE cte.RN = 1"; + + /// + /// Gets all data update records within a date range, ordered by StartDT descending. + /// + public const string SqlGetDataUpdatesInRange = @" + SELECT du.SourceSystem, + du.SourceData, + du.TableName, + du.StartDT, + du.EndDT, + du.UpdateType, + du.WasSuccessful, + du.NumberRecords + FROM dbo.DataUpdate AS du + WHERE du.StartDT >= @minDt + AND du.StartDT <= @maxDt + ORDER BY du.StartDT DESC"; } diff --git a/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.DataSync.cs b/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.DataSync.cs index 5bc44cc..98acd1f 100644 --- a/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.DataSync.cs +++ b/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.DataSync.cs @@ -20,4 +20,17 @@ public partial class LotFinderRepository commandTimeout: _options.Value.DefaultTimeoutSeconds)).ToList(), ct); } + + /// + public async Task> GetDataUpdatesInRangeAsync(DateTime minDt, DateTime maxDt, CancellationToken ct = default) + { + return await ExecuteQueryAsync( + nameof(GetDataUpdatesInRangeAsync), + "SQL_GET_DATA_UPDATES_IN_RANGE", + async connection => (await connection.QueryAsync( + LotFinderQueries.SqlGetDataUpdatesInRange, + new { minDt, maxDt }, + commandTimeout: _options.Value.DefaultTimeoutSeconds)).ToList(), + ct); + } } diff --git a/NEW/tests/JdeScoping.Api.Tests/Controllers/ManualSyncControllerTests.cs b/NEW/tests/JdeScoping.Api.Tests/Controllers/ManualSyncControllerTests.cs index 93e3080..89d764f 100644 --- a/NEW/tests/JdeScoping.Api.Tests/Controllers/ManualSyncControllerTests.cs +++ b/NEW/tests/JdeScoping.Api.Tests/Controllers/ManualSyncControllerTests.cs @@ -111,11 +111,13 @@ public class ManualSyncControllerTests var okResult = (OkObjectResult)result.Result!; var viewModels = okResult.Value.ShouldBeAssignableTo>()!; viewModels.Count.ShouldBe(2); - viewModels[0].Name.ShouldBe("WorkOrders"); + viewModels[0].Name.ShouldBe("Items"); viewModels[0].SupportedSyncTypes.ShouldContain("mass"); viewModels[0].SupportedSyncTypes.ShouldContain("daily"); - viewModels[0].SupportedSyncTypes.ShouldContain("hourly"); - viewModels[1].Name.ShouldBe("Items"); + viewModels[1].Name.ShouldBe("WorkOrders"); + viewModels[1].SupportedSyncTypes.ShouldContain("mass"); + viewModels[1].SupportedSyncTypes.ShouldContain("daily"); + viewModels[1].SupportedSyncTypes.ShouldContain("hourly"); } [Fact]