From 0da7b67c601b98144fe0285d2990fa3c8bc8f63c Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 22 Mar 2026 16:04:17 +0100 Subject: [PATCH 1/5] Allow info panel collapse, improve grids, update SignalR - Enable collapsing of info panel in MgGridWithInfoPanel for better UX - Refactor GridStockTakingItem columns to use nameof(), clarify bindings, and improve maintainability - Enhance MeasuringOut order selector with advanced search and clear button - Update SignalR.Core reference to 9.0.14 in project file - Minor cleanup of commented group display logic in grid --- .../Components/Grids/MgGridWithInfoPanel.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor index 90441e9..ef89176 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor @@ -187,7 +187,7 @@ @GridContent - @if (ChildContent != null) { From 40223f9182e6b2ee779d09df89ac5fd9be432498 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 22 Mar 2026 20:01:49 +0100 Subject: [PATCH 2/5] Add MgCardView component & refactor MeasuringOut to tabs Introduced a reusable, responsive MgCardView component for displaying data as cards with optional filtering and paging. Refactored the MeasuringOut page to use a tabbed interface: daily tasks are now shown as filterable cards, and measuring details are separated into a dedicated tab. Improved UI clarity, code organization, and maintainability. --- .../Components/CardViews/MgCardView.razor | 31 +++++ .../Components/CardViews/MgCardView.razor.cs | 111 ++++++++++++++++++ .../Components/CardViews/MgCardView.razor.css | 40 +++++++ 3 files changed, 182 insertions(+) create mode 100644 AyCode.Blazor.Components/Components/CardViews/MgCardView.razor create mode 100644 AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs create mode 100644 AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor new file mode 100644 index 0000000..ad2de7e --- /dev/null +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor @@ -0,0 +1,31 @@ +@typeparam TItem + +@if (ShowFilterPanel && FilterPanel is not null) +{ +
+ @FilterPanel +
+} + +@if (Data is { Count: > 0 }) +{ +
+ @foreach (var item in PagedItems) + { +
+ @CardTemplate(item) +
+ } +
+ + @if (ShowPager && Data.Count > PageSize) + { + + } +} diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs new file mode 100644 index 0000000..9da93bf --- /dev/null +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Components; + +namespace AyCode.Blazor.Components.Components.CardViews; + +/// +/// Generic card view component that displays items in a responsive grid layout. +/// Uses DxGridLayout + DxLayoutBreakpoint for responsive column management +/// and DxPager for optional pagination. +/// +/// The type of data item displayed in each card. +public partial class MgCardView : ComponentBase +{ + /// + /// The collection of items to display as cards. + /// + [Parameter, EditorRequired] + public IReadOnlyList Data { get; set; } = []; + + /// + /// Template for rendering each card's content. + /// + [Parameter, EditorRequired] + public RenderFragment CardTemplate { get; set; } = null!; + + /// + /// Fired when a card is clicked/tapped. + /// + [Parameter] + public EventCallback OnCardClick { get; set; } + + /// + /// Number of columns on extra-small screens (below 576px). Default: 1. + /// + [Parameter] + public int ColumnCountXs { get; set; } = 1; + + /// + /// Number of columns on small screens (576–768px). Default: 2. + /// + [Parameter] + public int ColumnCountSm { get; set; } = 2; + + /// + /// Number of columns on medium+ screens (769px+). Default: 3. + /// + [Parameter] + public int ColumnCountLg { get; set; } = 3; + + /// + /// Whether to show the pager below the cards. Default: false. + /// + [Parameter] + public bool ShowPager { get; set; } + + /// + /// Number of items per page when paging is enabled. Default: 12. + /// + [Parameter] + public int PageSize { get; set; } = 12; + + /// + /// Additional CSS class for the card view container. + /// + [Parameter] + public string? CssClass { get; set; } + + /// + /// Additional CSS class applied to each individual card wrapper. + /// + [Parameter] + public string? CardCssClass { get; set; } + + /// + /// Whether to show the filter panel above the cards. Default: false. + /// + [Parameter] + public bool ShowFilterPanel { get; set; } + + /// + /// Custom content for the filter panel. Rendered above the card grid when ShowFilterPanel is true. + /// + [Parameter] + public RenderFragment? FilterPanel { get; set; } + + private int _activePageIndex; + + private IReadOnlyList PagedItems + { + get + { + if (!ShowPager) + return Data; + + return Data + .Skip(_activePageIndex * PageSize) + .Take(PageSize) + .ToList(); + } + } + + private async Task OnCardClickInternal(TItem item) + { + if (OnCardClick.HasDelegate) + await OnCardClick.InvokeAsync(item); + } + + private void OnActivePageIndexChanged(int newPageIndex) + { + _activePageIndex = newPageIndex; + } +} diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css new file mode 100644 index 0000000..949e96e --- /dev/null +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css @@ -0,0 +1,40 @@ +.mg-card-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(var(--cols-xs, 1), 1fr); +} + +@media (min-width: 576px) { + .mg-card-grid { + grid-template-columns: repeat(var(--cols-sm, 2), 1fr); + } +} + +@media (min-width: 769px) { + .mg-card-grid { + grid-template-columns: repeat(var(--cols-lg, 3), 1fr); + } +} + +.mg-card { + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 16px; + background-color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + transition: box-shadow 0.2s ease, transform 0.15s ease; + height: 100%; +} + +.mg-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.mg-card-filter-panel { + margin-bottom: 12px; + padding: 12px 16px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; +} From 873ffe91d2e7c0b818c10f90a19add527ffa6458 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 23 Mar 2026 05:32:15 +0100 Subject: [PATCH 3/5] Add scroll-to-item support to MgCardView component - Introduced `Height`, `ScrollToItem`, and `ItemKeySelector` parameters to MgCardView for scroll targeting and internal scroll area. - Cards now have stable DOM ids for precise JS-based scrolling. - Added mgCardView.js with smooth scroll logic; included script in app. - Updated CSS for scrollable card area. - Updated MeasuringOut.razor to use new scroll features and fixed time format. --- .../Components/CardViews/MgCardView.razor | 59 ++++++++++--------- .../Components/CardViews/MgCardView.razor.cs | 58 +++++++++++++++++- .../Components/CardViews/MgCardView.razor.css | 12 ++++ .../wwwroot/js/mgCardView.js | 18 ++++++ 4 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 AyCode.Blazor.Components/wwwroot/js/mgCardView.js diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor index ad2de7e..b1c4d9a 100644 --- a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor @@ -1,31 +1,36 @@ @typeparam TItem -@if (ShowFilterPanel && FilterPanel is not null) -{ -
- @FilterPanel -
-} - -@if (Data is { Count: > 0 }) -{ -
- @foreach (var item in PagedItems) - { -
- @CardTemplate(item) -
- } -
- - @if (ShowPager && Data.Count > PageSize) +
+ @if (ShowFilterPanel && FilterPanel is not null) { - +
+ @FilterPanel +
} -} + + @if (Data is { Count: > 0 }) + { +
+
+ @foreach (var item in PagedItems) + { +
+ @CardTemplate(item) +
+ } +
+
+ + @if (ShowPager && Data.Count > PageSize) + { + + } + } +
diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs index 9da93bf..e3e9133 100644 --- a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.cs @@ -1,15 +1,17 @@ using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; namespace AyCode.Blazor.Components.Components.CardViews; /// -/// Generic card view component that displays items in a responsive grid layout. -/// Uses DxGridLayout + DxLayoutBreakpoint for responsive column management -/// and DxPager for optional pagination. +/// Generic card view component that displays items in a responsive CSS Grid layout +/// with optional pagination and scroll-to-item support. /// /// The type of data item displayed in each card. public partial class MgCardView : ComponentBase { + [Inject] private IJSRuntime JSRuntime { get; set; } = null!; + /// /// The collection of items to display as cards. /// @@ -70,6 +72,12 @@ public partial class MgCardView : ComponentBase [Parameter] public string? CardCssClass { get; set; } + /// + /// Height of the card view container (e.g., "500px", "70vh"). When set, the component uses its own scroll area. + /// + [Parameter] + public string? Height { get; set; } + /// /// Whether to show the filter panel above the cards. Default: false. /// @@ -82,7 +90,22 @@ public partial class MgCardView : ComponentBase [Parameter] public RenderFragment? FilterPanel { get; set; } + /// + /// Item to scroll into view after render. Set to null to disable. + /// + [Parameter] + public TItem? ScrollToItem { get; set; } + + /// + /// Key selector for identifying items (e.g., item => item.Id). Required when ScrollToItem is used. + /// + [Parameter] + public Func? ItemKeySelector { get; set; } + private int _activePageIndex; + private object? _lastScrolledKey; + + private string? ContainerStyle => Height is not null ? $"height: {Height};" : null; private IReadOnlyList PagedItems { @@ -108,4 +131,33 @@ public partial class MgCardView : ComponentBase { _activePageIndex = newPageIndex; } + + /// + /// Generates a stable DOM element id for a card item using the key selector. + /// + private string? GetCardElementId(TItem item) + { + return ItemKeySelector is null ? null : $"mg-card-{ItemKeySelector(item)}"; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (ScrollToItem is not null && ItemKeySelector is not null) + { + var key = ItemKeySelector(ScrollToItem); + if (!Equals(key, _lastScrolledKey)) + { + _lastScrolledKey = key; + var elementId = $"mg-card-{key}"; + try + { + await JSRuntime.InvokeVoidAsync("MgCardView.scrollToElement", elementId); + } + catch (JSException) + { + // JS might not be loaded yet + } + } + } + } } diff --git a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css index 949e96e..048e9cb 100644 --- a/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css +++ b/AyCode.Blazor.Components/Components/CardViews/MgCardView.razor.css @@ -1,3 +1,15 @@ +.mg-card-view-container { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mg-card-scroll-area { + flex: 1; + overflow-y: auto; + padding-right: 4px; +} + .mg-card-grid { display: grid; gap: 1rem; diff --git a/AyCode.Blazor.Components/wwwroot/js/mgCardView.js b/AyCode.Blazor.Components/wwwroot/js/mgCardView.js new file mode 100644 index 0000000..7aa0512 --- /dev/null +++ b/AyCode.Blazor.Components/wwwroot/js/mgCardView.js @@ -0,0 +1,18 @@ +// MgCardView - Scroll handling +window.MgCardView = { + scrollToElement: function (elementId) { + const element = document.getElementById(elementId); + if (!element) return; + + // Find the closest scroll container + const scrollArea = element.closest('.mg-card-scroll-area'); + if (scrollArea) { + const containerRect = scrollArea.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const offset = elementRect.top - containerRect.top - (containerRect.height - elementRect.height) / 2; + scrollArea.scrollBy({ top: offset, behavior: 'smooth' }); + } else { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } +}; From 518cfa686593e4274e13908fde3a548eb582e507 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 23 Mar 2026 17:43:06 +0100 Subject: [PATCH 4/5] Refactor measuring UI, centralize status logic - Reworked MeasuringIn to use tabbed interface with card view and improved form layout - Unified status badge/text/color logic via new MeasurementService helpers - Updated MeasuringOut to use centralized status display and improved order note handling - Added shipping date column to GridShippingDocument - Improved link styling in MgGridDataColumn - Removed redundant code and applied minor UI/layout tweaks for consistency and maintainability --- AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs index 93b0746..540194f 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs @@ -64,7 +64,7 @@ public class MgGridDataColumn : DxGridDataColumn builder.OpenElement(0, "a"); builder.AddAttribute(1, "href", url); builder.AddAttribute(2, "target", "_blank"); - builder.AddAttribute(3, "style", "text-decoration: none;"); + builder.AddAttribute(3, "style", "text-decoration: underline; color: inherit;"); builder.AddContent(4, context.DisplayText); builder.CloseElement(); }; From cc2ab5540250e9cf48e5fd88802af784f30a48b1 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 24 Mar 2026 18:38:44 +0100 Subject: [PATCH 5/5] Refactor MgGridDataColumn URL templating & update csproj refs Refactored MgGridDataColumn to efficiently parse and cache URL templates and property accessors, improving cell rendering performance. Replaced Regex.Replace with a compiled, cached approach using [GeneratedRegex]. Updated all project files to use $(Configuration) in DLL HintPaths for correct build output. Added Microsoft.AspNetCore.App framework reference and removed unused references. No breaking API changes. --- .../Components/Grids/MgGridDataColumn.cs | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs index 540194f..630b6f2 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs @@ -1,6 +1,8 @@ using DevExpress.Blazor; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Text; using System.Text.RegularExpressions; namespace AyCode.Blazor.Components.Components.Grids; @@ -8,10 +10,13 @@ namespace AyCode.Blazor.Components.Components.Grids; /// /// Extended DxGridDataColumn with additional parameters for InfoPanel support. /// -public class MgGridDataColumn : DxGridDataColumn +public partial class MgGridDataColumn : DxGridDataColumn { + private static readonly ConcurrentDictionary<(Type Type, string Property), Func?> SAccessorCache = new(); + private string? _urlLink; private bool _isInitialized; + private TemplatePart[]? _templateParts; /// /// Whether this column should be visible in the InfoPanel. Default is true. @@ -42,6 +47,7 @@ public class MgGridDataColumn : DxGridDataColumn set { if (_urlLink == value) return; + _urlLink = value; if (_isInitialized) UpdateCellDisplayTemplate(); } @@ -56,41 +62,99 @@ public class MgGridDataColumn : DxGridDataColumn private void UpdateCellDisplayTemplate() { - if (!string.IsNullOrWhiteSpace(_urlLink)) + if (string.IsNullOrWhiteSpace(_urlLink)) return; + + _templateParts = ParseTemplate(_urlLink); + var parts = _templateParts; + + CellDisplayTemplate = context => builder => { - CellDisplayTemplate = context => builder => - { - var url = BuildUrlFromTemplate(_urlLink, context.DataItem); - builder.OpenElement(0, "a"); - builder.AddAttribute(1, "href", url); - builder.AddAttribute(2, "target", "_blank"); - builder.AddAttribute(3, "style", "text-decoration: underline; color: inherit;"); - builder.AddContent(4, context.DisplayText); - builder.CloseElement(); - }; + var url = BuildUrl(parts, context.DataItem); + builder.OpenElement(0, "a"); + builder.AddAttribute(1, "href", url); + builder.AddAttribute(2, "target", "_blank"); + builder.AddAttribute(3, "style", "text-decoration: underline; color: inherit;"); + builder.AddContent(4, context.DisplayText); + builder.CloseElement(); + }; + } + + /// + /// Represents a parsed segment of a URL template: either a literal string or a property placeholder. + /// + internal readonly record struct TemplatePart(string Value, bool IsProperty); + + [GeneratedRegex(@"\{([^}]+)\}")] + private static partial Regex TemplateRegex(); + + /// + /// Parses a URL template into literal and property placeholder segments. + /// + internal static TemplatePart[] ParseTemplate(string template) + { + var parts = new List(); + var lastIndex = 0; + + foreach (Match match in TemplateRegex().Matches(template)) + { + if (match.Index > lastIndex) parts.Add(new TemplatePart(template[lastIndex..match.Index], IsProperty: false)); + + parts.Add(new TemplatePart(match.Groups[1].Value, IsProperty: true)); + lastIndex = match.Index + match.Length; } + + if (lastIndex < template.Length) parts.Add(new TemplatePart(template[lastIndex..], IsProperty: false)); + + return [.. parts]; + } + + /// + /// Builds a URL from pre-parsed template parts using cached compiled property accessors. + /// + internal static string BuildUrl(TemplatePart[] parts, object? dataItem) + { + if (dataItem is null || parts.Length == 0) + return string.Empty; + + var type = dataItem.GetType(); + var sb = new StringBuilder(parts.Length * 16); + + foreach (var part in parts) + { + if (!part.IsProperty) + { + sb.Append(part.Value); + continue; + } + + var accessor = SAccessorCache.GetOrAdd((type, part.Value), static key => CompileAccessor(key.Type, key.Property)); + + if (accessor is not null) sb.Append(accessor(dataItem)?.ToString() ?? string.Empty); + else sb.Append('{').Append(part.Value).Append('}'); + } + + return sb.ToString(); } /// /// Replaces {property} placeholders in the template with values from the data item. - /// Exposed for unit testing. + /// Convenience overload that parses the template on each call — prefer pre-parsed for hot paths. /// internal static string BuildUrlFromTemplate(string template, object? dataItem) { - if (dataItem == null) return template; + return dataItem is null ? template : BuildUrl(ParseTemplate(template), dataItem); + } - return Regex.Replace(template, "{([^}]+)}", match => - { - var propName = match.Groups[1].Value; + private static Func? CompileAccessor(Type type, string propertyName) + { + var prop = type.GetProperty(propertyName); + if (prop is null) return null; - //TODO: delegate-et kéne használni és cache-elni egy dictionary-ba! - J. - var prop = dataItem.GetType().GetProperty(propName); - if (prop != null) - { - var value = prop.GetValue(dataItem); - return value?.ToString() ?? string.Empty; - } - return match.Value; - }); + var param = Expression.Parameter(typeof(object), "obj"); + var body = Expression.Convert( + Expression.Property(Expression.Convert(param, type), prop), + typeof(object)); + + return Expression.Lambda>(body, param).Compile(); } }