From 566a0ae0e5a9561b0f934de9d8e4e49030909c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:02:03 +0300 Subject: [PATCH] =?UTF-8?q?PredifinedReports=20i=C3=A7in=20tasar=C4=B1m=20?= =?UTF-8?q?ve=20d=C3=BCzenleme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PredefinedReports/DynamicFormReport.cs | 21 +- .../PredefinedReports/DynamicGridReport.cs | 30 +- .../PredefinedReports/DynamicReport.cs | 39 +- .../DynamicReportImageHelper.cs | 343 +++++++++++++++++- .../DynamicReportSqlBuilder.cs | 15 +- .../PredefinedReports/DynamicTreeReport.cs | 29 +- .../CustomReportStorageWebExtension.cs | 7 +- ui/src/views/list/useFilters.tsx | 3 +- .../views/report/DevexpressReportDesigner.tsx | 24 -- .../views/report/DevexpressReportViewer.tsx | 28 +- ui/src/views/report/reportRouteParams.ts | 7 +- 11 files changed, 450 insertions(+), 96 deletions(-) diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 21f4bdc..daa88b7 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -20,6 +20,7 @@ using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Sozsoft.Platform.Localization; using Sozsoft.Platform.Queries; +using Volo.Abp.Users; namespace Sozsoft.Reports.PredefinedReports; @@ -76,6 +77,7 @@ public class DynamicFormReport : XtraReport var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); var configuration = scope.ServiceProvider.GetRequiredService(); + var currentUser = scope.ServiceProvider.GetRequiredService(); if (!await authManager.CanAccess(listFormCode)) { @@ -117,7 +119,7 @@ public class DynamicFormReport : XtraReport ConfigurePage(); ConfigureDataSource(selectQuery, connectionString, dataSourceType); - await BuildLayoutAsync(listForm, reportFields, row, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration)); + await BuildLayoutAsync(listForm, reportFields, row, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName); RequestParameters = false; } @@ -155,7 +157,8 @@ public class DynamicFormReport : XtraReport IReadOnlyList fields, IDictionary row, IStringLocalizer localizer, - string imageBaseUrl) + string imageBaseUrl, + string currentUserName) { Bands.Clear(); var formGroups = GetFormGroups(listForm, fields).ToList(); @@ -176,19 +179,15 @@ public class DynamicFormReport : XtraReport Font = new DXFont("Arial", 14F, DXFontStyle.Bold), TextAlignment = TextAlignment.MiddleLeft }); + DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth); + + DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth); + DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth); - bottomMargin.Controls.Add(new XRPageInfo - { - PageInfo = PageInfo.DateTime, - TextFormatString = "{0:dd.MM.yyyy HH:mm}", - BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth / 2, 18), - Font = new DXFont("Arial", 7.5F), - TextAlignment = TextAlignment.MiddleLeft - }); bottomMargin.Controls.Add(new XRPageInfo { TextFormatString = "Sayfa {0} / {1}", - BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 0, pageWidth / 2, 18), + BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18), Font = new DXFont("Arial", 8F), TextAlignment = TextAlignment.MiddleRight }); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 32859ab..f2914e4 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -18,6 +18,7 @@ using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Sozsoft.Platform.Localization; using Sozsoft.Platform.Queries; +using Volo.Abp.Users; namespace Sozsoft.Reports.PredefinedReports; @@ -69,6 +70,7 @@ public class DynamicGridReport : XtraReport var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); var configuration = scope.ServiceProvider.GetRequiredService(); + var currentUser = scope.ServiceProvider.GetRequiredService(); if (!await authManager.CanAccess(listFormCode)) { @@ -142,6 +144,11 @@ public class DynamicGridReport : XtraReport defaultValueHelper, dataSourceType); var imageBaseUrl = DynamicReportImageHelper.GetImageBaseUrl(configuration); + var criteriaText = DynamicReportImageHelper.BuildReportCriteriaText( + queryParams.Filter, + queryParams.Sort, + fields, + localizer); reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl); ConfigurePage(reportColumns); @@ -149,7 +156,7 @@ public class DynamicGridReport : XtraReport selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, reportColumns, localizer, imageBaseUrl); + BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText); RequestParameters = false; } @@ -236,13 +243,15 @@ public class DynamicGridReport : XtraReport ListForm listForm, IReadOnlyList fields, IStringLocalizer localizer, - string imageBaseUrl) + string imageBaseUrl, + string currentUserName, + string criteriaText) { Bands.Clear(); var topMargin = new TopMarginBand { HeightF = Margins.Top }; var bottomMargin = new BottomMarginBand { HeightF = 32 }; - var reportHeader = new ReportHeaderBand { HeightF = 32 }; + var reportHeader = new ReportHeaderBand { HeightF = string.IsNullOrWhiteSpace(criteriaText) ? 32 : 50 }; var pageHeader = new PageHeaderBand { HeightF = 28 }; var detailHeight = fields.Any(x => x.IsImage) ? DynamicReportImageHelper.GetGridImageHeight() @@ -262,21 +271,16 @@ public class DynamicGridReport : XtraReport Font = new DXFont("Arial", 14F, DXFontStyle.Bold), TextAlignment = TextAlignment.MiddleLeft }); + DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth); + DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth); - var generatedAt = new XRPageInfo - { - PageInfo = PageInfo.DateTime, - TextFormatString = "{0:dd.MM.yyyy HH:mm}", - BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth / 2, 18), - Font = new DXFont("Arial", 7.5F), - TextAlignment = TextAlignment.MiddleLeft - }; - bottomMargin.Controls.Add(generatedAt); + DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth); + DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth); var pageInfo = new XRPageInfo { TextFormatString = "Sayfa {0} / {1}", - BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 0, pageWidth / 2, 18), + BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18), Font = new DXFont("Arial", 8F), TextAlignment = TextAlignment.MiddleRight }; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs index 38ba5ba..8fa2a13 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs @@ -6,6 +6,7 @@ using DevExpress.XtraReports.UI; using Sozsoft.Languages.Entities; using Sozsoft.Platform.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Users; namespace Sozsoft.Reports.PredefinedReports; @@ -35,10 +36,46 @@ public partial class DynamicReport : XtraReport // Language verilerini yükle LoadLanguageParameters(); + AddHeaderGeneratedAt(); + AddFooterCurrentUserName(); + AddFooterLine(); - pageInfo1.TextFormatString = "{0:d}"; + pageInfo1.Visible = false; xrRichText1.BeforePrint += XrRichText1_BeforePrint; } + + private void AddFooterLine() + { + var pageWidth = PageWidth - Margins.Left - Margins.Right; + DynamicReportImageHelper.AddFooterTopLine(BottomMargin.Controls, pageWidth); + pageInfo1.TopF = 8; + pageInfo2.TopF = 8; + } + + private void AddHeaderGeneratedAt() + { + var pageWidth = PageWidth - Margins.Left - Margins.Right; + DynamicReportImageHelper.AddGeneratedAtToHeader(ReportHeader.Controls, pageWidth); + } + + private void AddFooterCurrentUserName() + { + try + { + if (_serviceProvider == null) + { + return; + } + + using var scope = _serviceProvider.CreateScope(); + var currentUser = scope.ServiceProvider.GetRequiredService(); + var pageWidth = PageWidth - Margins.Left - Margins.Right; + DynamicReportImageHelper.AddCurrentUserNameToFooter(BottomMargin.Controls, currentUser.UserName, pageWidth); + } + catch + { + } + } private void LoadLanguageParameters() { diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs index 47923aa..075a122 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs @@ -34,7 +34,7 @@ internal static class DynamicReportImageHelper private const string ImageSourceFunctionName = "SozReportImageSource"; private const string FormatValueFunctionName = "SozReportFormatValue"; private static readonly Regex ReportPagingRegex = new( - @"\s+ORDER\s+BY[\s\S]*?(?:\s+OFFSET\s+\d+\s+ROWS(?:\s+FETCH\s+NEXT\s+\d+\s+ROWS\s+ONLY)?)?\s*$", + @"\s+OFFSET\s+\d+\s+ROWS(?:\s+FETCH\s+NEXT\s+\d+\s+ROWS\s+ONLY)?\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); private static readonly HttpClient HttpClient = new(new HttpClientHandler { @@ -271,6 +271,94 @@ internal static class DynamicReportImageHelper return Localize(localizer, caption, field.FieldName); } + public static void AddGeneratedAtToHeader(XRControlCollection controls, float pageWidth) + { + var width = Math.Min(260F, pageWidth * 0.4F); + controls.Add(new XRPageInfo + { + PageInfo = PageInfo.DateTime, + TextFormatString = "{0:dd.MM.yyyy HH:mm}", + BoundsF = new System.Drawing.RectangleF(pageWidth - width, 0, width, 18), + Font = new DevExpress.Drawing.DXFont("Arial", 8F), + TextAlignment = TextAlignment.MiddleRight, + CanGrow = false + }); + } + + public static void AddCurrentUserNameToFooter(XRControlCollection controls, string userName, float pageWidth) + { + if (string.IsNullOrWhiteSpace(userName)) + { + return; + } + + controls.Add(new XRLabel + { + Text = userName, + BoundsF = new System.Drawing.RectangleF(0, 8, pageWidth / 2, 18), + Font = new DevExpress.Drawing.DXFont("Arial", 7.5F), + TextAlignment = TextAlignment.MiddleLeft, + CanGrow = false, + Multiline = false + }); + } + + public static void AddReportCriteriaToHeader( + XRControlCollection controls, + string criteriaText, + float pageWidth) + { + if (string.IsNullOrWhiteSpace(criteriaText)) + { + return; + } + + controls.Add(new XRLabel + { + Text = criteriaText, + BoundsF = new System.Drawing.RectangleF(0, 28, pageWidth, 18), + Font = new DevExpress.Drawing.DXFont("Arial", 7.5F), + ForeColor = System.Drawing.Color.FromArgb(100, 116, 139), + TextAlignment = TextAlignment.MiddleLeft, + CanGrow = true, + Multiline = true + }); + } + + public static void AddFooterTopLine(XRControlCollection controls, float pageWidth) + { + controls.Add(new XRLine + { + BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth, 2), + ForeColor = System.Drawing.Color.FromArgb(203, 213, 225), + LineWidth = 1 + }); + } + + public static string BuildReportCriteriaText( + string filter, + string sort, + IEnumerable fields, + IStringLocalizer localizer) + { + var parts = new List(); + var fieldCaptions = CreateFieldCaptionMap(fields, localizer); + var filterText = FormatFilterText(filter, fieldCaptions); + var sortText = FormatSortText(sort, fieldCaptions); + + if (!string.IsNullOrWhiteSpace(filterText)) + { + parts.Add("Filtre: " + filterText); + } + + if (!string.IsNullOrWhiteSpace(sortText)) + { + parts.Add("Sıralama: " + sortText); + } + + return string.Join(" | ", parts); + } + public static string Localize(IStringLocalizer localizer, string key, string fallback) { if (string.IsNullOrWhiteSpace(key)) @@ -485,7 +573,7 @@ internal static class DynamicReportImageHelper string editorType, out string text) { - return TryFormatReportValue(value, field.SourceDbType, field.Format, editorType, out text); + return TryFormatReportValue(value, field.SourceDbType, GetEffectiveFormat(field), editorType, out text); } public static string CreateFormatValueExpression( @@ -493,7 +581,7 @@ internal static class DynamicReportImageHelper ListFormField field, string editorType) { - return $"{FormatValueFunctionName}({valueExpression}, '{field.SourceDbType}', '{EscapeExpressionString(field.Format)}', '{EscapeExpressionString(editorType)}')"; + return $"{FormatValueFunctionName}({valueExpression}, '{field.SourceDbType}', '{EscapeExpressionString(GetEffectiveFormat(field))}', '{EscapeExpressionString(editorType)}')"; } public static string FormatReportDate(DateTime value) @@ -627,6 +715,190 @@ internal static class DynamicReportImageHelper : value ? "Yes" : "No"; } + private static Dictionary CreateFieldCaptionMap( + IEnumerable fields, + IStringLocalizer localizer) + { + return (fields ?? []) + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName)) + .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + x => x.Key, + x => GetLocalizedFieldCaption(x.First(), localizer), + StringComparer.OrdinalIgnoreCase); + } + + private static string FormatSortText(string sort, IReadOnlyDictionary fieldCaptions) + { + if (string.IsNullOrWhiteSpace(sort)) + { + return null; + } + + var items = sort + .Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries) + .Select(item => + { + var parts = item.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + return null; + } + + var fieldName = parts[0].Trim('"'); + var direction = parts.Length > 1 ? parts[1] : "asc"; + var directionText = string.Equals(direction, "desc", StringComparison.OrdinalIgnoreCase) + ? "azalan" + : "artan"; + + return $"{GetFieldCaption(fieldName, fieldCaptions)} {directionText}"; + }) + .Where(x => !string.IsNullOrWhiteSpace(x)); + + return string.Join(", ", items); + } + + private static string FormatFilterText(string filter, IReadOnlyDictionary fieldCaptions) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(filter); + return FormatFilterElement(document.RootElement, fieldCaptions); + } + catch (JsonException) + { + return null; + } + } + + private static string FormatFilterElement(JsonElement element, IReadOnlyDictionary fieldCaptions) + { + if (element.ValueKind != JsonValueKind.Array) + { + return FormatJsonValue(element); + } + + var items = element.EnumerateArray().ToList(); + if (items.Count == 0) + { + return null; + } + + if (items.Count >= 3 && + items[0].ValueKind == JsonValueKind.String && + items[1].ValueKind == JsonValueKind.String) + { + return FormatFilterCondition(items[0].GetString(), items[1].GetString(), items[2], fieldCaptions); + } + + if (items.Count == 2 && + items[0].ValueKind == JsonValueKind.String && + items[0].GetString() == "!") + { + var text = FormatFilterElement(items[1], fieldCaptions); + return string.IsNullOrWhiteSpace(text) ? null : "değil (" + text + ")"; + } + + var parts = new List(); + for (var i = 0; i < items.Count; i++) + { + if (items[i].ValueKind == JsonValueKind.String && + IsFilterConnector(items[i].GetString())) + { + parts.Add(FormatFilterConnector(items[i].GetString())); + continue; + } + + var text = FormatFilterElement(items[i], fieldCaptions); + if (!string.IsNullOrWhiteSpace(text)) + { + parts.Add(text); + } + } + + return string.Join(" ", parts); + } + + private static string FormatFilterCondition( + string fieldName, + string operation, + JsonElement value, + IReadOnlyDictionary fieldCaptions) + { + var caption = GetFieldCaption(fieldName, fieldCaptions); + var op = FormatFilterOperation(operation); + var formattedValue = FormatJsonValue(value); + + return string.IsNullOrWhiteSpace(formattedValue) + ? $"{caption} {op}" + : $"{caption} {op} {formattedValue}"; + } + + private static string FormatFilterOperation(string operation) + { + return operation?.ToLowerInvariant() switch + { + "=" => "=", + "<>" => "eşit değil", + ">" => ">", + ">=" => ">=", + "<" => "<", + "<=" => "<=", + "contains" => "içerir", + "notcontains" => "içermez", + "startswith" => "ile başlar", + "endswith" => "ile biter", + "between" => "arasında", + "anyof" => "şunlardan biri", + "noneof" => "şunlardan hiçbiri", + "isblank" => "boş", + "isnotblank" => "boş değil", + _ => operation + }; + } + + private static string FormatJsonValue(JsonElement value) + { + return value.ValueKind switch + { + JsonValueKind.String => value.GetString(), + JsonValueKind.Number => value.GetRawText(), + JsonValueKind.True => "Evet", + JsonValueKind.False => "Hayır", + JsonValueKind.Null => string.Empty, + JsonValueKind.Array => string.Join(", ", value.EnumerateArray().Select(FormatJsonValue)), + _ => value.ToString() + }; + } + + private static string GetFieldCaption(string fieldName, IReadOnlyDictionary fieldCaptions) + { + if (string.IsNullOrWhiteSpace(fieldName)) + { + return fieldName; + } + + return fieldCaptions.TryGetValue(fieldName, out var caption) + ? caption + : fieldName; + } + + private static bool IsFilterConnector(string value) + { + return string.Equals(value, "and", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "or", StringComparison.OrdinalIgnoreCase); + } + + private static string FormatFilterConnector(string value) + { + return string.Equals(value, "or", StringComparison.OrdinalIgnoreCase) ? "veya" : "ve"; + } + private static bool IsBooleanField(string editorType, DbType dbType) { return editorType is PlatformConsts.EditorTypes.dxCheckBox or PlatformConsts.EditorTypes.dxSwitch || @@ -635,12 +907,70 @@ internal static class DynamicReportImageHelper private static string GetCustomFormat(string format) { - return string.IsNullOrWhiteSpace(format) ? null : format; + if (string.IsNullOrWhiteSpace(format)) + { + return null; + } + + var customFormat = TryGetFormatOption(format, "displayFormat") ?? + TryGetFormatOption(format, "format") ?? + format.Trim(); + + return NormalizeDateTimeFormat(customFormat); + } + + private static string GetEffectiveFormat(ListFormField field) + { + return TryGetFormatOption(field?.EditorOptions, "displayFormat") ?? + TryGetFormatOption(field?.EditorOptions, "format") ?? + field?.Format; + } + + private static string TryGetFormatOption(string json, string propertyName) + { + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(json); + if (document.RootElement.ValueKind != JsonValueKind.Object || + !TryGetLookupProperty(document.RootElement, propertyName, out var value)) + { + return null; + } + + return value.ValueKind == JsonValueKind.String ? value.GetString() : null; + } + catch (JsonException) + { + return null; + } + } + + private static string NormalizeDateTimeFormat(string format) + { + if (string.IsNullOrWhiteSpace(format)) + { + return null; + } + + return format.Trim() switch + { + PlatformConsts.GridColumnOptions.ColumnValueFormatShortDate => "d", + PlatformConsts.GridColumnOptions.ColumnValueFormatShortTime => "t", + PlatformConsts.GridColumnOptions.ColumnValueFormatLongTime => "T", + PlatformConsts.GridColumnOptions.ColumnValueFormatShortDateShortTime => "g", + PlatformConsts.GridColumnOptions.ColumnValueFormatLongDateLongTime => "G", + _ => format.Trim() + }; } private static bool IsDateField(DbType dbType) { - return dbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset; + return dbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset or DbType.Time; } private static bool TryConvertToDateTime(object value, out DateTime date) @@ -653,6 +983,9 @@ internal static class DynamicReportImageHelper case DateTimeOffset dateTimeOffset: date = dateTimeOffset.LocalDateTime; return true; + case TimeSpan timeSpan: + date = DateTime.Today.Add(timeSpan); + return true; } var text = Convert.ToString(value, CultureInfo.InvariantCulture); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs index 728a7b9..2785006 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs @@ -60,7 +60,14 @@ internal static class DynamicReportSqlBuilder } var sourceAlias = QuoteSqlIdentifier(SourceAlias); - return $"SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts)} FROM ({NormalizeSql(selectQuery)}) AS {sourceAlias}"; + var normalizedSql = NormalizeSql(selectQuery); + var orderBy = ExtractOrderBy(normalizedSql); + if (!string.IsNullOrWhiteSpace(orderBy)) + { + normalizedSql = OrderByRegex.Replace(normalizedSql, string.Empty).Trim(); + } + + return $"SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts)} FROM ({normalizedSql}) AS {sourceAlias}{orderBy}"; } private static string CreateLookupSelectPart( @@ -139,6 +146,12 @@ internal static class DynamicReportSqlBuilder return sql.Trim().TrimEnd(';'); } + private static string ExtractOrderBy(string sql) + { + var match = OrderByRegex.Match(sql); + return match.Success ? " " + match.Value.Trim() : string.Empty; + } + private static string QuoteSqlIdentifier(string identifier) { return $"\"{identifier.Replace("\"", "\"\"")}\""; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs index af2597c..a372444 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -19,6 +19,7 @@ using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Sozsoft.Platform.Localization; using Sozsoft.Platform.Queries; +using Volo.Abp.Users; namespace Sozsoft.Reports.PredefinedReports; @@ -73,6 +74,7 @@ public class DynamicTreeReport : XtraReport var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); var configuration = scope.ServiceProvider.GetRequiredService(); + var currentUser = scope.ServiceProvider.GetRequiredService(); if (!await authManager.CanAccess(listFormCode)) { @@ -153,13 +155,18 @@ public class DynamicTreeReport : XtraReport dataSourceType); var imageBaseUrl = DynamicReportImageHelper.GetImageBaseUrl(configuration); + var criteriaText = DynamicReportImageHelper.BuildReportCriteriaText( + queryParams.Filter, + queryParams.Sort, + fields, + localizer); var sourceRows = await ReadRowsAsync(dynamicDataRepository, selectQuery, connectionString); var orderedRows = OrderTreeRows(sourceRows, treeOptions).ToList(); reportColumns = ApplyImageCounts(reportColumns, orderedRows, imageBaseUrl); ConfigurePage(reportColumns); ConfigureDataSource(orderedRows, reportColumns); - BuildLayout(listForm, reportColumns, localizer, imageBaseUrl); + BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText); RequestParameters = false; } @@ -408,13 +415,15 @@ public class DynamicTreeReport : XtraReport ListForm listForm, IReadOnlyList fields, IStringLocalizer localizer, - string imageBaseUrl) + string imageBaseUrl, + string currentUserName, + string criteriaText) { Bands.Clear(); var topMargin = new TopMarginBand { HeightF = Margins.Top }; var bottomMargin = new BottomMarginBand { HeightF = 32 }; - var reportHeader = new ReportHeaderBand { HeightF = 32 }; + var reportHeader = new ReportHeaderBand { HeightF = string.IsNullOrWhiteSpace(criteriaText) ? 32 : 50 }; var pageHeader = new PageHeaderBand { HeightF = 28 }; var detailHeight = fields.Any(x => x.IsImage) ? DynamicReportImageHelper.GetGridImageHeight() @@ -434,20 +443,16 @@ public class DynamicTreeReport : XtraReport Font = new DXFont("Arial", 14F, DXFontStyle.Bold), TextAlignment = TextAlignment.MiddleLeft }); + DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth); + DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth); - bottomMargin.Controls.Add(new XRPageInfo - { - PageInfo = PageInfo.DateTime, - TextFormatString = "{0:dd.MM.yyyy HH:mm}", - BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth / 2, 18), - Font = new DXFont("Arial", 7.5F), - TextAlignment = TextAlignment.MiddleLeft - }); + DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth); + DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth); bottomMargin.Controls.Add(new XRPageInfo { TextFormatString = "Sayfa {0} / {1}", - BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 0, pageWidth / 2, 18), + BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18), Font = new DXFont("Arial", 8F), TextAlignment = TextAlignment.MiddleRight }); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index e9eb924..4cd2fb9 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -282,7 +282,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens var keyValue = pair.Split('=', 2); if (keyValue.Length == 2 && !string.IsNullOrWhiteSpace(keyValue[0])) { - parameters[keyValue[0]] = keyValue[1]; + parameters[DecodeQueryComponent(keyValue[0])] = DecodeQueryComponent(keyValue[1]); } } } @@ -375,6 +375,11 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens return decoded; } + private static string DecodeQueryComponent(string value) + { + return DecodeRepeatedly(value ?? string.Empty); + } + public override Dictionary GetUrls() { // Returns a dictionary that contains the report names (URLs) and display names. diff --git a/ui/src/views/list/useFilters.tsx b/ui/src/views/list/useFilters.tsx index 1702cba..44a380a 100644 --- a/ui/src/views/list/useFilters.tsx +++ b/ui/src/views/list/useFilters.tsx @@ -22,6 +22,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant' import { GanttRef } from 'devextreme-react/cjs/gantt' import { useStoreState } from '@/store' import { MULTIVALUE_DELIMITER } from '@/constants/app.constant' +import { createReportQueryString } from '../report/reportRouteParams' export interface ISelectBoxData { value?: string @@ -208,7 +209,7 @@ const createDynamicListReportUrl = ( searchParams.set('sort', sort) } - const queryString = searchParams.toString() + const queryString = createReportQueryString(searchParams) return queryString ? `${reportUrl}?${queryString}` : reportUrl } diff --git a/ui/src/views/report/DevexpressReportDesigner.tsx b/ui/src/views/report/DevexpressReportDesigner.tsx index 41a3669..4f20293 100644 --- a/ui/src/views/report/DevexpressReportDesigner.tsx +++ b/ui/src/views/report/DevexpressReportDesigner.tsx @@ -58,30 +58,6 @@ const loadViewerCss = (isDarkMode: boolean) => { if (themeLink.href !== themeHref) { themeLink.href = themeHref } - - if (!document.getElementById(VIEWER_DARK_OVERRIDES_ID)) { - const style = document.createElement('style') - style.id = VIEWER_DARK_OVERRIDES_ID - style.textContent = ` - .report-viewer-shell { - min-height: calc(100vh - 4rem); - } - - .report-viewer-shell-dark { - background: #111827; - color: #e5e7eb; - } - - .report-viewer-shell-dark .dxrd-preview, - .report-viewer-shell-dark .dxrd-preview-wrapper, - .report-viewer-shell-dark .dxrd-toolbar-wrapper, - .report-viewer-shell-dark .dxrd-right-panel, - .report-viewer-shell-dark .dxrd-search-wrapper { - background-color: #111827; - } - ` - document.head.appendChild(style) - } } const DevexpressReportDesigner: React.FC = () => { diff --git a/ui/src/views/report/DevexpressReportViewer.tsx b/ui/src/views/report/DevexpressReportViewer.tsx index 0768500..8945078 100644 --- a/ui/src/views/report/DevexpressReportViewer.tsx +++ b/ui/src/views/report/DevexpressReportViewer.tsx @@ -12,7 +12,7 @@ import { ZoomAutoBy } from 'devexpress-reporting/viewer/constants' import { APP_NAME } from '@/constants/app.constant' import { MODE_DARK } from '@/constants/theme.constant' import { useStoreState } from '@/store' -import { createReportUrl } from './reportRouteParams' +import { createReportQueryString, createReportUrl } from './reportRouteParams' const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme' const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides' @@ -60,30 +60,6 @@ const loadViewerCss = (isDarkMode: boolean) => { if (themeLink.href !== themeHref) { themeLink.href = themeHref } - - if (!document.getElementById(VIEWER_DARK_OVERRIDES_ID)) { - const style = document.createElement('style') - style.id = VIEWER_DARK_OVERRIDES_ID - style.textContent = ` - .report-viewer-shell { - min-height: calc(100vh - 4rem); - } - - .report-viewer-shell-dark { - background: #111827; - color: #e5e7eb; - } - - .report-viewer-shell-dark .dxrd-preview, - .report-viewer-shell-dark .dxrd-preview-wrapper, - .report-viewer-shell-dark .dxrd-toolbar-wrapper, - .report-viewer-shell-dark .dxrd-right-panel, - .report-viewer-shell-dark .dxrd-search-wrapper { - background-color: #111827; - } - ` - document.head.appendChild(style) - } } const DevexpressReportViewer: React.FC = () => { @@ -177,7 +153,7 @@ const DevexpressReportViewer: React.FC = () => { searchParams.set('cultureName', reportCultureName) } - const search = searchParams.toString() + const search = createReportQueryString(searchParams) return createReportUrl({ report, id, listFormCode }, search ? `?${search}` : '') }, [report, id, listFormCode, location.search, reportCultureName]) diff --git a/ui/src/views/report/reportRouteParams.ts b/ui/src/views/report/reportRouteParams.ts index a54f538..ee6a949 100644 --- a/ui/src/views/report/reportRouteParams.ts +++ b/ui/src/views/report/reportRouteParams.ts @@ -30,6 +30,11 @@ export const normalizeReportRouteParams = ({ } } +export const createReportQueryString = (searchParams: URLSearchParams) => + Array.from(searchParams.entries()) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&') + export const createReportUrl = ( params: ReportRouteParams, search: string, @@ -47,6 +52,6 @@ export const createReportUrl = ( if (listFormCode) pathSegments.push(listFormCode) const path = pathSegments.map((segment) => encodeURIComponent(segment)).join('/') - const queryString = searchParams.toString() + const queryString = createReportQueryString(searchParams) return queryString ? `${path}?${queryString}` : path }