Add DevLoader project reference

This commit is contained in:
Joseph Doherty
2026-02-13 10:11:01 -05:00
parent 1b9367dcbb
commit ddc782dc76
13 changed files with 296 additions and 151 deletions
+1
View File
@@ -15,6 +15,7 @@
<Project Path="src/Utils/JdeScoping.ConfigManager.Core/JdeScoping.ConfigManager.Core.csproj" />
<Project Path="src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj" />
<Project Path="src/Utils/JdeScoping.ConfigManager.Ui/JdeScoping.ConfigManager.Ui.csproj" />
<Project Path="src/Utils/JdeScoping.DevLoader/JdeScoping.DevLoader.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/JdeScoping.Api.Tests/JdeScoping.Api.Tests.csproj" />
@@ -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();
@@ -34,20 +34,15 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
[Parameter]
public bool IsReadOnly { get; set; }
/// <summary>
/// The current search text.
/// </summary>
protected string SearchText { get; set; } = "";
/// <summary>
/// The search results from the API.
/// </summary>
protected List<TItem> SearchResults { get; set; } = [];
/// <summary>
/// The currently selected item from search results.
/// The currently selected value from the dropdown.
/// </summary>
protected TItem? SelectedItem { get; set; }
protected TItem? SelectedValue { get; set; }
/// <summary>
/// Reference to the data grid for explicit refresh.
@@ -93,13 +88,6 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <returns>The unique key value.</returns>
protected abstract object GetItemKey(TItem item);
/// <summary>
/// Gets the display text value for an item (used for matching in autocomplete).
/// </summary>
/// <param name="item">The item to get the display text for.</param>
/// <returns>The display text value.</returns>
protected abstract string GetDisplayText(TItem item);
/// <summary>
/// Handles the autocomplete search.
/// </summary>
@@ -107,7 +95,7 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <returns>A task representing the asynchronous search operation.</returns>
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<TItem> : ComponentBase where T
}
}
/// <summary>
/// Handles selection from the autocomplete.
/// </summary>
/// <param name="value">The selected value from the autocomplete control.</param>
protected void OnItemSelected(object value)
{
if (value is string text && !string.IsNullOrEmpty(text))
{
SelectedItem = SearchResults.FirstOrDefault(i => GetDisplayText(i) == text);
}
else
{
SelectedItem = null;
}
}
/// <summary>
/// Adds the selected item to the list.
/// </summary>
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;
}
/// <summary>
@@ -24,19 +24,17 @@
@if (!IsReadOnly)
{
<RadzenRow Gap="0.5rem" class="rz-mb-3">
<RadzenColumn Size="10">
<RadzenFormField Text="Item Number" Style="width: 100%;">
<RadzenAutoComplete @bind-Value="_searchText" Data="@_searchResults" TextProperty="ItemNumber"
LoadData="@OnSearchAsync" MinLength="3" Placeholder="Search items (3+ chars)..."
Style="width: 100%;" Change="@OnItemSelected" />
</RadzenFormField>
</RadzenColumn>
<RadzenColumn Size="2">
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(_selectedItem == null)" Style="margin-top: 24px;" />
</RadzenColumn>
</RadzenRow>
<div class="filter-input-row rz-mb-3">
<div class="filter-input-col">
<label class="field-label">Item Number</label>
<RadzenDropDown @bind-Value="_selectedValue" Data="@_searchResults" TextProperty="ItemNumber"
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true" AllowClear="true"
Placeholder="Search items (2+ chars)..." Style="width: 100%;" />
</div>
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(_selectedValue == null)" />
</div>
}
<RadzenDataGrid @ref="_grid" Data="@Items" TItem="ItemViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
@@ -72,9 +70,8 @@
[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;
private string _searchText = "";
private List<ItemViewModel> _searchResults = [];
private ItemViewModel? _selectedItem;
private ItemViewModel? _selectedValue;
private bool _isUploading;
private RadzenDataGrid<ItemViewModel>? _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)
@@ -15,19 +15,17 @@
@if (!IsReadOnly)
{
<RadzenRow Gap="0.5rem" class="rz-mb-3">
<RadzenColumn Size="10">
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
Style="width: 100%;" Change="@OnItemSelected" />
</RadzenFormField>
</RadzenColumn>
<RadzenColumn Size="2">
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
</RadzenColumn>
</RadzenRow>
<div class="filter-input-row rz-mb-3">
<div class="filter-input-col">
<label class="field-label">@SearchFieldLabel</label>
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true" AllowClear="true"
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
</div>
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedValue == null)" />
</div>
}
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="OperatorViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
@@ -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<List<OperatorViewModel>> SearchApiAsync(string filter)
{
@@ -14,19 +14,17 @@
@if (!IsReadOnly)
{
<RadzenRow Gap="0.5rem" class="rz-mb-3">
<RadzenColumn Size="10">
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
Style="width: 100%;" Change="@OnItemSelected" />
</RadzenFormField>
</RadzenColumn>
<RadzenColumn Size="2">
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
</RadzenColumn>
</RadzenRow>
<div class="filter-input-row rz-mb-3">
<div class="filter-input-col">
<label class="field-label">@SearchFieldLabel</label>
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true" AllowClear="true"
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
</div>
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedValue == null)" />
</div>
}
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="ProfitCenterViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
@@ -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<List<ProfitCenterViewModel>> SearchApiAsync(string filter)
{
@@ -14,19 +14,17 @@
@if (!IsReadOnly)
{
<RadzenRow Gap="0.5rem" class="rz-mb-3">
<RadzenColumn Size="10">
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
Style="width: 100%;" Change="@OnItemSelected" />
</RadzenFormField>
</RadzenColumn>
<RadzenColumn Size="2">
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
</RadzenColumn>
</RadzenRow>
<div class="filter-input-row rz-mb-3">
<div class="filter-input-col">
<label class="field-label">@SearchFieldLabel</label>
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true" AllowClear="true"
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
</div>
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
Disabled="@(SelectedValue == null)" />
</div>
}
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="WorkCenterViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
@@ -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<List<WorkCenterViewModel>> SearchApiAsync(string filter)
{
@@ -7,61 +7,55 @@
@namespace JdeScoping.Client.Components.Search
<RadzenCard class="rz-mb-4">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-3">Search Details</RadzenText>
<div class="search-details-header">
<strong>Search Details</strong>
</div>
<RadzenRow Gap="1rem">
<RadzenColumn Size="12">
<RadzenFormField Text="Search Type" Style="width: 100%;">
<RadzenDropDown @bind-Value="SelectedSearchType" Data="@ValidCombinations" TextProperty="Name" ValueProperty="Id"
Placeholder="Select type" Disabled="@Search.IsReadOnly" Change="@OnSearchTypeChangedHandler" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">Search Type</label>
<RadzenDropDown @bind-Value="SelectedSearchType" Data="@ValidCombinations" TextProperty="Name" ValueProperty="Id"
Placeholder="Select type" Disabled="@Search.IsReadOnly" Change="@OnSearchTypeChangedHandler" Style="width: 100%;" />
</RadzenColumn>
</RadzenRow>
<RadzenRow Gap="1rem" class="rz-mt-3">
<RadzenColumn Size="12">
<RadzenFormField Text="Name" Style="width: 100%;">
<RadzenTextBox @bind-Value="Search.Name" Disabled="@Search.IsReadOnly" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">Name</label>
<RadzenTextBox @bind-Value="Search.Name" Disabled="@Search.IsReadOnly" Style="width: 100%;" />
<ValidationMessage For="@(() => Search.Name)" class="validation-message text-danger" />
</RadzenColumn>
</RadzenRow>
<RadzenRow Gap="1rem" class="rz-mt-3">
<RadzenColumn Size="4">
<RadzenFormField Text="Submitted At" Style="width: 100%;">
<RadzenTextBox Value="@(Search.SubmitDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">Submitted At</label>
<RadzenTextBox Value="@(Search.SubmitDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
</RadzenColumn>
<RadzenColumn Size="4">
<RadzenFormField Text="Started At" Style="width: 100%;">
<RadzenTextBox Value="@(Search.StartDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">Started At</label>
<RadzenTextBox Value="@(Search.StartDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
</RadzenColumn>
<RadzenColumn Size="4">
<RadzenFormField Text="Completed At" Style="width: 100%;">
<RadzenTextBox Value="@(Search.EndDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">Completed At</label>
<RadzenTextBox Value="@(Search.EndDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
</RadzenColumn>
</RadzenRow>
<RadzenRow Gap="1rem" class="rz-mt-3">
<RadzenColumn Size="4">
<RadzenFormField Text="User" Style="width: 100%;">
<RadzenTextBox Value="@Search.UserName" ReadOnly="true" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">User</label>
<RadzenTextBox Value="@Search.UserName" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
</RadzenColumn>
<RadzenColumn Size="4">
<RadzenFormField Text="Status" Style="width: 100%;">
<RadzenTextBox Value="@Search.Status" ReadOnly="true" Style="@($"width: 100%; background-color: {Search.StatusColor};")" />
</RadzenFormField>
<label class="field-label">Status</label>
<RadzenTextBox Value="@Search.Status" ReadOnly="true" Style="@($"width: 100%; background-color: {Search.StatusColor};")" />
</RadzenColumn>
<RadzenColumn Size="4">
@if (Search.HasResults)
{
<RadzenFormField Text=" " Style="width: 100%;">
<RadzenButton Text="Download Results" Icon="download" ButtonStyle="ButtonStyle.Success" Click="@OnDownloadResults" Style="width: 100%;" />
</RadzenFormField>
<label class="field-label">&nbsp;</label>
<RadzenButton Text="Download Results" Icon="download" ButtonStyle="ButtonStyle.Success" Click="@OnDownloadResults" Style="width: 100%;" />
}
</RadzenColumn>
</RadzenRow>
@@ -29,7 +29,7 @@
<RadzenText TextStyle="TextStyle.H4" class="rz-m-0">Search</RadzenText>
@if (!_search.IsReadOnly)
{
<RadzenButton Text="Submit" Icon="send" ButtonStyle="ButtonStyle.Primary" Click="@SubmitSearchAsync" IsBusy="@_isSubmitting" BusyText="Submitting..." />
<RadzenButton Text="Submit" Icon="send" ButtonStyle="ButtonStyle.Primary" Click="@SubmitSearchAsync" IsBusy="@_isSubmitting" BusyText="Submitting..." class="btn-submit-blue" />
}
</RadzenStack>
@@ -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;
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>jdescoping-devloader</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.Console" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\JdeScoping.DataSync.Dev\JdeScoping.DataSync.Dev.csproj" />
</ItemGroup>
</Project>
@@ -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 <path> [--connection-string <cs>]");
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<DevEtlRegistry>();
var pipelineLogger = loggerFactory.CreateLogger<JdeScoping.DataSync.Etl.Pipeline.EtlPipeline>();
// --- 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;
@@ -0,0 +1,39 @@
using JdeScoping.DataAccess.Interfaces;
using Microsoft.Data.SqlClient;
using Oracle.ManagedDataAccess.Client;
namespace JdeScoping.DevLoader;
/// <summary>
/// Minimal connection factory for the dev loader.
/// Only supports SQL Server (LotFinder cache); Oracle methods throw NotSupportedException.
/// </summary>
public class SimpleDbConnectionFactory : IDbConnectionFactory
{
private readonly string _sqlConnectionString;
public SimpleDbConnectionFactory(string sqlConnectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sqlConnectionString);
_sqlConnectionString = sqlConnectionString;
}
public async Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default)
{
var conn = new SqlConnection(_sqlConnectionString);
await conn.OpenAsync(ct);
return conn;
}
public Task<OracleConnection> CreateJdeConnectionAsync(CancellationToken ct = default)
=> throw new NotSupportedException("Oracle JDE connections are not available in the dev loader.");
public Task<OracleConnection> CreateJdeStageConnectionAsync(CancellationToken ct = default)
=> throw new NotSupportedException("Oracle JDE Stage connections are not available in the dev loader.");
public Task<OracleConnection> CreateCmsConnectionAsync(CancellationToken ct = default)
=> throw new NotSupportedException("Oracle CMS connections are not available in the dev loader.");
public Task<OracleConnection> CreateGiwConnectionAsync(CancellationToken ct = default)
=> throw new NotSupportedException("Oracle GIW connections are not available in the dev loader.");
}