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.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"> </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.");
|
||||
}
|
||||
Reference in New Issue
Block a user