Add DevLoader project reference
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
<Project Path="src/Utils/JdeScoping.ConfigManager.Core/JdeScoping.ConfigManager.Core.csproj" />
|
<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.Cli/JdeScoping.ConfigManager.Cli.csproj" />
|
||||||
<Project Path="src/Utils/JdeScoping.ConfigManager.Ui/JdeScoping.ConfigManager.Ui.csproj" />
|
<Project Path="src/Utils/JdeScoping.ConfigManager.Ui/JdeScoping.ConfigManager.Ui.csproj" />
|
||||||
|
<Project Path="src/Utils/JdeScoping.DevLoader/JdeScoping.DevLoader.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/JdeScoping.Api.Tests/JdeScoping.Api.Tests.csproj" />
|
<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)
|
// 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);
|
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
|
var aggregated = updates
|
||||||
.GroupBy(u => new DateTime(u.StartDt.Year, u.StartDt.Month, u.StartDt.Day, u.StartDt.Hour, u.StartDt.Minute, 0))
|
.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,
|
var items = g.GroupBy(u => u.TableName)
|
||||||
EndDt = g.Max(u => u.EndDt),
|
.Select(tg => new DataUpdateItemDto
|
||||||
WasSuccessful = g.All(u => u.WasSuccessful),
|
{
|
||||||
PassedCount = g.Count(u => u.WasSuccessful),
|
TableName = tg.Key,
|
||||||
FailedCount = g.Count(u => !u.WasSuccessful),
|
WasSuccessful = tg.All(u => u.WasSuccessful),
|
||||||
Items = g.Select(u => new DataUpdateItemDto
|
NumberRecords = tg.Sum(u => Math.Max(0, u.NumberRecords))
|
||||||
|
}).OrderBy(i => i.TableName).ToList();
|
||||||
|
|
||||||
|
return new DataUpdateDto
|
||||||
{
|
{
|
||||||
TableName = u.TableName,
|
StartDt = g.Key,
|
||||||
WasSuccessful = u.WasSuccessful,
|
EndDt = g.Max(u => u.EndDt),
|
||||||
NumberRecords = u.NumberRecords
|
WasSuccessful = items.All(i => i.WasSuccessful),
|
||||||
}).OrderBy(i => i.TableName).ToList()
|
PassedCount = items.Count(i => i.WasSuccessful),
|
||||||
|
FailedCount = items.Count(i => !i.WasSuccessful),
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.OrderByDescending(d => d.StartDt)
|
.OrderByDescending(d => d.StartDt)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -34,20 +34,15 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsReadOnly { get; set; }
|
public bool IsReadOnly { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current search text.
|
|
||||||
/// </summary>
|
|
||||||
protected string SearchText { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The search results from the API.
|
/// The search results from the API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<TItem> SearchResults { get; set; } = [];
|
protected List<TItem> SearchResults { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently selected item from search results.
|
/// The currently selected value from the dropdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected TItem? SelectedItem { get; set; }
|
protected TItem? SelectedValue { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reference to the data grid for explicit refresh.
|
/// 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>
|
/// <returns>The unique key value.</returns>
|
||||||
protected abstract object GetItemKey(TItem item);
|
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>
|
/// <summary>
|
||||||
/// Handles the autocomplete search.
|
/// Handles the autocomplete search.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -107,7 +95,7 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
|
|||||||
/// <returns>A task representing the asynchronous search operation.</returns>
|
/// <returns>A task representing the asynchronous search operation.</returns>
|
||||||
protected async Task OnSearchAsync(LoadDataArgs args)
|
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);
|
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>
|
/// <summary>
|
||||||
/// Adds the selected item to the list.
|
/// Adds the selected item to the list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected async Task AddItemAsync()
|
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));
|
var isDuplicate = Items.Any(i => GetItemKey(i).Equals(selectedKey));
|
||||||
|
|
||||||
if (!isDuplicate)
|
if (!isDuplicate)
|
||||||
{
|
{
|
||||||
Items.Add(SelectedItem);
|
Items.Add(SelectedValue);
|
||||||
await ItemsChanged.InvokeAsync(Items);
|
await ItemsChanged.InvokeAsync(Items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SearchText = "";
|
SelectedValue = null;
|
||||||
SelectedItem = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -24,19 +24,17 @@
|
|||||||
|
|
||||||
@if (!IsReadOnly)
|
@if (!IsReadOnly)
|
||||||
{
|
{
|
||||||
<RadzenRow Gap="0.5rem" class="rz-mb-3">
|
<div class="filter-input-row rz-mb-3">
|
||||||
<RadzenColumn Size="10">
|
<div class="filter-input-col">
|
||||||
<RadzenFormField Text="Item Number" Style="width: 100%;">
|
<label class="field-label">Item Number</label>
|
||||||
<RadzenAutoComplete @bind-Value="_searchText" Data="@_searchResults" TextProperty="ItemNumber"
|
<RadzenDropDown @bind-Value="_selectedValue" Data="@_searchResults" TextProperty="ItemNumber"
|
||||||
LoadData="@OnSearchAsync" MinLength="3" Placeholder="Search items (3+ chars)..."
|
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
|
||||||
Style="width: 100%;" Change="@OnItemSelected" />
|
AllowFiltering="true" AllowClear="true"
|
||||||
</RadzenFormField>
|
Placeholder="Search items (2+ chars)..." Style="width: 100%;" />
|
||||||
</RadzenColumn>
|
</div>
|
||||||
<RadzenColumn Size="2">
|
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
||||||
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
Disabled="@(_selectedValue == null)" />
|
||||||
Disabled="@(_selectedItem == null)" Style="margin-top: 24px;" />
|
</div>
|
||||||
</RadzenColumn>
|
|
||||||
</RadzenRow>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<RadzenDataGrid @ref="_grid" Data="@Items" TItem="ItemViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
|
<RadzenDataGrid @ref="_grid" Data="@Items" TItem="ItemViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
|
||||||
@@ -72,9 +70,8 @@
|
|||||||
[Inject]
|
[Inject]
|
||||||
private IJSRuntime JSRuntime { get; set; } = default!;
|
private IJSRuntime JSRuntime { get; set; } = default!;
|
||||||
|
|
||||||
private string _searchText = "";
|
|
||||||
private List<ItemViewModel> _searchResults = [];
|
private List<ItemViewModel> _searchResults = [];
|
||||||
private ItemViewModel? _selectedItem;
|
private ItemViewModel? _selectedValue;
|
||||||
private bool _isUploading;
|
private bool _isUploading;
|
||||||
private RadzenDataGrid<ItemViewModel>? _grid;
|
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()
|
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);
|
await ItemsChanged.InvokeAsync(Items);
|
||||||
}
|
}
|
||||||
_searchText = "";
|
_selectedValue = null;
|
||||||
_selectedItem = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteItem(ItemViewModel item)
|
private async Task DeleteItem(ItemViewModel item)
|
||||||
|
|||||||
@@ -15,19 +15,17 @@
|
|||||||
|
|
||||||
@if (!IsReadOnly)
|
@if (!IsReadOnly)
|
||||||
{
|
{
|
||||||
<RadzenRow Gap="0.5rem" class="rz-mb-3">
|
<div class="filter-input-row rz-mb-3">
|
||||||
<RadzenColumn Size="10">
|
<div class="filter-input-col">
|
||||||
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
|
<label class="field-label">@SearchFieldLabel</label>
|
||||||
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
||||||
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
|
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
|
||||||
Style="width: 100%;" Change="@OnItemSelected" />
|
AllowFiltering="true" AllowClear="true"
|
||||||
</RadzenFormField>
|
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
|
||||||
</RadzenColumn>
|
</div>
|
||||||
<RadzenColumn Size="2">
|
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
||||||
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
Disabled="@(SelectedValue == null)" />
|
||||||
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
|
</div>
|
||||||
</RadzenColumn>
|
|
||||||
</RadzenRow>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="OperatorViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
|
<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 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 SearchFieldLabel => "Name";
|
||||||
protected override string AutocompleteTextProperty => "FullName";
|
protected override string AutocompleteTextProperty => "FullName";
|
||||||
protected override string ClearConfirmMessage => "Are you sure you want to clear all operators?";
|
protected override string ClearConfirmMessage => "Are you sure you want to clear all operators?";
|
||||||
|
|
||||||
protected override object GetItemKey(OperatorViewModel item) => item.UserId;
|
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)
|
protected override async Task<List<OperatorViewModel>> SearchApiAsync(string filter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,19 +14,17 @@
|
|||||||
|
|
||||||
@if (!IsReadOnly)
|
@if (!IsReadOnly)
|
||||||
{
|
{
|
||||||
<RadzenRow Gap="0.5rem" class="rz-mb-3">
|
<div class="filter-input-row rz-mb-3">
|
||||||
<RadzenColumn Size="10">
|
<div class="filter-input-col">
|
||||||
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
|
<label class="field-label">@SearchFieldLabel</label>
|
||||||
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
||||||
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
|
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
|
||||||
Style="width: 100%;" Change="@OnItemSelected" />
|
AllowFiltering="true" AllowClear="true"
|
||||||
</RadzenFormField>
|
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
|
||||||
</RadzenColumn>
|
</div>
|
||||||
<RadzenColumn Size="2">
|
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
||||||
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
Disabled="@(SelectedValue == null)" />
|
||||||
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
|
</div>
|
||||||
</RadzenColumn>
|
|
||||||
</RadzenRow>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="ProfitCenterViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
|
<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 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 SearchFieldLabel => "Profit Center";
|
||||||
protected override string AutocompleteTextProperty => "Code";
|
protected override string AutocompleteTextProperty => "Code";
|
||||||
protected override string ClearConfirmMessage => "Are you sure you want to clear all profit centers?";
|
protected override string ClearConfirmMessage => "Are you sure you want to clear all profit centers?";
|
||||||
|
|
||||||
protected override object GetItemKey(ProfitCenterViewModel item) => item.Code;
|
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)
|
protected override async Task<List<ProfitCenterViewModel>> SearchApiAsync(string filter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,19 +14,17 @@
|
|||||||
|
|
||||||
@if (!IsReadOnly)
|
@if (!IsReadOnly)
|
||||||
{
|
{
|
||||||
<RadzenRow Gap="0.5rem" class="rz-mb-3">
|
<div class="filter-input-row rz-mb-3">
|
||||||
<RadzenColumn Size="10">
|
<div class="filter-input-col">
|
||||||
<RadzenFormField Text="@SearchFieldLabel" Style="width: 100%;">
|
<label class="field-label">@SearchFieldLabel</label>
|
||||||
<RadzenAutoComplete @bind-Value="SearchText" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
<RadzenDropDown @bind-Value="SelectedValue" Data="@SearchResults" TextProperty="@AutocompleteTextProperty"
|
||||||
LoadData="@OnSearchAsync" MinLength="3" Placeholder="@SearchPlaceholder"
|
LoadData="@OnSearchAsync" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
|
||||||
Style="width: 100%;" Change="@OnItemSelected" />
|
AllowFiltering="true" AllowClear="true"
|
||||||
</RadzenFormField>
|
Placeholder="@SearchPlaceholder" Style="width: 100%;" />
|
||||||
</RadzenColumn>
|
</div>
|
||||||
<RadzenColumn Size="2">
|
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
||||||
<RadzenButton Text="Add" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@AddItemAsync"
|
Disabled="@(SelectedValue == null)" />
|
||||||
Disabled="@(SelectedItem == null)" Style="margin-top: 24px;" />
|
</div>
|
||||||
</RadzenColumn>
|
|
||||||
</RadzenRow>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<RadzenDataGrid @ref="Grid" Data="@Items" TItem="WorkCenterViewModel" AllowSorting="true" Style="min-height: 150px; max-height: 300px;">
|
<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 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 SearchFieldLabel => "Work Center";
|
||||||
protected override string AutocompleteTextProperty => "Code";
|
protected override string AutocompleteTextProperty => "Code";
|
||||||
protected override string ClearConfirmMessage => "Are you sure you want to clear all work centers?";
|
protected override string ClearConfirmMessage => "Are you sure you want to clear all work centers?";
|
||||||
|
|
||||||
protected override object GetItemKey(WorkCenterViewModel item) => item.Code;
|
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)
|
protected override async Task<List<WorkCenterViewModel>> SearchApiAsync(string filter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,61 +7,55 @@
|
|||||||
@namespace JdeScoping.Client.Components.Search
|
@namespace JdeScoping.Client.Components.Search
|
||||||
|
|
||||||
<RadzenCard class="rz-mb-4">
|
<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">
|
<RadzenRow Gap="1rem">
|
||||||
<RadzenColumn Size="12">
|
<RadzenColumn Size="12">
|
||||||
<RadzenFormField Text="Search Type" Style="width: 100%;">
|
<label class="field-label">Search Type</label>
|
||||||
<RadzenDropDown @bind-Value="SelectedSearchType" Data="@ValidCombinations" TextProperty="Name" ValueProperty="Id"
|
<RadzenDropDown @bind-Value="SelectedSearchType" Data="@ValidCombinations" TextProperty="Name" ValueProperty="Id"
|
||||||
Placeholder="Select type" Disabled="@Search.IsReadOnly" Change="@OnSearchTypeChangedHandler" Style="width: 100%;" />
|
Placeholder="Select type" Disabled="@Search.IsReadOnly" Change="@OnSearchTypeChangedHandler" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
</RadzenRow>
|
</RadzenRow>
|
||||||
|
|
||||||
<RadzenRow Gap="1rem" class="rz-mt-3">
|
<RadzenRow Gap="1rem" class="rz-mt-3">
|
||||||
<RadzenColumn Size="12">
|
<RadzenColumn Size="12">
|
||||||
<RadzenFormField Text="Name" Style="width: 100%;">
|
<label class="field-label">Name</label>
|
||||||
<RadzenTextBox @bind-Value="Search.Name" Disabled="@Search.IsReadOnly" Style="width: 100%;" />
|
<RadzenTextBox @bind-Value="Search.Name" Disabled="@Search.IsReadOnly" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
<ValidationMessage For="@(() => Search.Name)" class="validation-message text-danger" />
|
<ValidationMessage For="@(() => Search.Name)" class="validation-message text-danger" />
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
</RadzenRow>
|
</RadzenRow>
|
||||||
|
|
||||||
<RadzenRow Gap="1rem" class="rz-mt-3">
|
<RadzenRow Gap="1rem" class="rz-mt-3">
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
<RadzenFormField Text="Submitted At" Style="width: 100%;">
|
<label class="field-label">Submitted At</label>
|
||||||
<RadzenTextBox Value="@(Search.SubmitDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
|
<RadzenTextBox Value="@(Search.SubmitDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
<RadzenFormField Text="Started At" Style="width: 100%;">
|
<label class="field-label">Started At</label>
|
||||||
<RadzenTextBox Value="@(Search.StartDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
|
<RadzenTextBox Value="@(Search.StartDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
<RadzenFormField Text="Completed At" Style="width: 100%;">
|
<label class="field-label">Completed At</label>
|
||||||
<RadzenTextBox Value="@(Search.EndDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" Style="width: 100%;" />
|
<RadzenTextBox Value="@(Search.EndDt?.ToString("MM/dd/yyyy hh:mm:ss tt") ?? "")" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
</RadzenRow>
|
</RadzenRow>
|
||||||
|
|
||||||
<RadzenRow Gap="1rem" class="rz-mt-3">
|
<RadzenRow Gap="1rem" class="rz-mt-3">
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
<RadzenFormField Text="User" Style="width: 100%;">
|
<label class="field-label">User</label>
|
||||||
<RadzenTextBox Value="@Search.UserName" ReadOnly="true" Style="width: 100%;" />
|
<RadzenTextBox Value="@Search.UserName" ReadOnly="true" class="readonly-input" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
<RadzenFormField Text="Status" Style="width: 100%;">
|
<label class="field-label">Status</label>
|
||||||
<RadzenTextBox Value="@Search.Status" ReadOnly="true" Style="@($"width: 100%; background-color: {Search.StatusColor};")" />
|
<RadzenTextBox Value="@Search.Status" ReadOnly="true" Style="@($"width: 100%; background-color: {Search.StatusColor};")" />
|
||||||
</RadzenFormField>
|
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
<RadzenColumn Size="4">
|
<RadzenColumn Size="4">
|
||||||
@if (Search.HasResults)
|
@if (Search.HasResults)
|
||||||
{
|
{
|
||||||
<RadzenFormField Text=" " Style="width: 100%;">
|
<label class="field-label"> </label>
|
||||||
<RadzenButton Text="Download Results" Icon="download" ButtonStyle="ButtonStyle.Success" Click="@OnDownloadResults" Style="width: 100%;" />
|
<RadzenButton Text="Download Results" Icon="download" ButtonStyle="ButtonStyle.Success" Click="@OnDownloadResults" Style="width: 100%;" />
|
||||||
</RadzenFormField>
|
|
||||||
}
|
}
|
||||||
</RadzenColumn>
|
</RadzenColumn>
|
||||||
</RadzenRow>
|
</RadzenRow>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<RadzenText TextStyle="TextStyle.H4" class="rz-m-0">Search</RadzenText>
|
<RadzenText TextStyle="TextStyle.H4" class="rz-m-0">Search</RadzenText>
|
||||||
@if (!_search.IsReadOnly)
|
@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>
|
</RadzenStack>
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,53 @@ code {
|
|||||||
padding: 0;
|
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) */
|
/* RadzenUpload inline button style (no drop zone) */
|
||||||
.rz-upload-inline .rz-fileupload-buttonbar {
|
.rz-upload-inline .rz-fileupload-buttonbar {
|
||||||
padding: 0;
|
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.");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user