From 75b459d79eb93b77bb0fc01bd1feabbfa3c0ce76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Tue, 30 Jun 2026 22:20:01 +0300 Subject: [PATCH] =?UTF-8?q?List=20Report=20k=C4=B1sm=C4=B1ndaki=20Gruplar?= =?UTF-8?q?=20=C3=A7al=C4=B1=C5=9Ft=C4=B1r=C4=B1ld=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ListForms/ListFormSelectAppService.cs | 10 + .../PredefinedReports/DynamicFormReport.cs | 4 +- .../PredefinedReports/DynamicGridReport.cs | 207 ++++++++++++++---- .../PredefinedReports/DynamicReport.cs | 2 +- .../DynamicReportGridStateHelper.cs | 104 ++++++++- .../DynamicReportSqlBuilder.cs | 125 +++++++++-- .../PredefinedReports/DynamicTreeReport.cs | 4 +- ui/public/version.json | 15 +- 8 files changed, 405 insertions(+), 66 deletions(-) diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs index ec3dbcd..1a619f8 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs @@ -399,6 +399,16 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe parameters.Add("@param" + i, input.Filters[i]); } } + + var cascadeParameterCount = lookup.CascadeParentFields? + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Length ?? 0; + + for (int i = input.Filters?.Length ?? 0; i < cascadeParameterCount; i++) + { + parameters.Add("@param" + i, null!); + } + var lookupQuery = defaultValueHelper.GetDefaultValue(lookup.LookupQuery); return await dynamicDataRepository.QueryAsync(lookupQuery, connectionString, parameters); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index b7873a1..d5a119a 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -129,7 +129,7 @@ public class DynamicFormReport : XtraReport Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; Landscape = false; - Font = new DXFont("Arial", 8.25F); + Font = new DXFont("Arial", 8F); } #pragma warning disable CA1416 @@ -206,7 +206,7 @@ public class DynamicFormReport : XtraReport { Text = DynamicReportImageHelper.Localize(localizer, group.Caption, group.Caption), BoundsF = new System.Drawing.RectangleF(0, y, pageWidth, 22), - Font = new DXFont("Arial", 9F, DXFontStyle.Bold), + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), BackColor = System.Drawing.Color.FromArgb(238, 242, 247), Padding = new PaddingInfo(5, 5, 2, 2), TextAlignment = TextAlignment.MiddleLeft diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index a6213b8..0d0de79 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -29,6 +29,7 @@ public class DynamicGridReport : XtraReport private const int A4PortraitWidth = 827; private const int HorizontalMargin = 50; private const int LandscapeColumnThreshold = 5; + private const float GroupIndentWidth = 18F; private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); private static IServiceScopeFactory _scopeFactory; @@ -122,21 +123,38 @@ public class DynamicGridReport : XtraReport .Take(8) .ToList(); } + var groupedFields = DynamicReportGridStateHelper.GetGroupedFields( + availableFields, + gridState?.CustomizationData, + queryParams.Group); + var groupedFieldNames = groupedFields + .Select(x => x.FieldName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var reportFields = groupedFieldNames.Count == 0 + ? visibleFields + : visibleFields + .Where(x => !groupedFieldNames.Contains(x.FieldName)) + .ToList(); + if (reportFields.Count == 0) + { + reportFields = visibleFields; + } - var reportColumns = visibleFields - .Select(field => - { - var selectField = selectFieldsByName[field.FieldName]; - editorTypesByField.TryGetValue(field.FieldName, out var editorType); - return CreateReportColumn( - field, - selectField, - defaultValueHelper, - editorType, - DynamicReportImageHelper.IsImageEditor(editorType), - editorType == PlatformConsts.EditorTypes.dxHtmlEditor); - }) - .Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined") + var groupColumns = CreateReportColumns( + groupedFields, + selectFieldsByName, + editorTypesByField, + defaultValueHelper); + var reportColumns = CreateReportColumns( + reportFields, + selectFieldsByName, + editorTypesByField, + defaultValueHelper); + + var allColumns = groupColumns + .Concat(reportColumns) + .GroupBy(x => x.BindingName, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) .ToList(); var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters( @@ -145,7 +163,7 @@ public class DynamicGridReport : XtraReport dataSourceType)); selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns( selectQuery, - reportColumns, + allColumns, defaultValueHelper, dataSourceType); var imageBaseUrl = DynamicReportImageHelper.GetImageBaseUrl(configuration); @@ -161,10 +179,33 @@ public class DynamicGridReport : XtraReport selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport); + BuildLayout(listForm, reportColumns, groupColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport); RequestParameters = false; } + private static List CreateReportColumns( + IEnumerable fields, + IReadOnlyDictionary selectFieldsByName, + IReadOnlyDictionary editorTypesByField, + DefaultValueHelper defaultValueHelper) + { + return fields + .Select(field => + { + var selectField = selectFieldsByName[field.FieldName]; + editorTypesByField.TryGetValue(field.FieldName, out var editorType); + return CreateReportColumn( + field, + selectField, + defaultValueHelper, + editorType, + DynamicReportImageHelper.IsImageEditor(editorType), + editorType == PlatformConsts.EditorTypes.dxHtmlEditor); + }) + .Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined") + .ToList(); + } + #pragma warning disable CA1416 private void ConfigurePage(IReadOnlyCollection visibleFields, bool isSubReport) { @@ -174,7 +215,7 @@ public class DynamicGridReport : XtraReport var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); Landscape = !isSubReport && (visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth); - Font = new DXFont("Arial", 8.25F); + Font = new DXFont("Arial", 8F); } private static System.Drawing.Printing.Margins CreateReportMargins( @@ -248,6 +289,7 @@ public class DynamicGridReport : XtraReport private void BuildLayout( ListForm listForm, IReadOnlyList fields, + IReadOnlyList groupFields, IStringLocalizer localizer, string imageBaseUrl, string currentUserName, @@ -256,6 +298,7 @@ public class DynamicGridReport : XtraReport { Bands.Clear(); + var pageWidth = PageWidth - Margins.Left - Margins.Right; var topMargin = new TopMarginBand { HeightF = Margins.Top }; var bottomMargin = new BottomMarginBand { HeightF = 32 }; var reportHeader = new ReportHeaderBand @@ -270,10 +313,15 @@ public class DynamicGridReport : XtraReport : 24; var detail = new DetailBand { HeightF = detailHeight }; var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 }; + var groupHeaderBands = CreateGroupHeaderBands(groupFields, localizer, pageWidth); - Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]); + Bands.AddRange([topMargin, reportHeader, pageHeader]); + foreach (var groupHeaderBand in groupHeaderBands.AsEnumerable().Reverse()) + { + Bands.Add(groupHeaderBand); + } + Bands.AddRange([detail, reportFooter, bottomMargin]); - var pageWidth = PageWidth - Margins.Left - Margins.Right; reportHeader.Controls.Add(new XRLabel { Text = DynamicReportImageHelper.GetLocalizedReportTitle(listForm, localizer), @@ -302,21 +350,24 @@ public class DynamicGridReport : XtraReport }; bottomMargin.Controls.Add(pageInfo); - var widths = CalculateWidths(fields, pageWidth); + var detailIndent = GetGroupDetailIndent(groupFields.Count, pageWidth); + var tableWidth = pageWidth - detailIndent; + var detailWidths = CalculateWidths(fields, tableWidth); + var headerWidths = CreateIndentedHeaderWidths(detailWidths, detailIndent); var headerTable = CreateTable(pageWidth, 28); var headerRow = new XRTableRow { HeightF = 28 }; - var detailTable = CreateTable(pageWidth, detailHeight); + var detailTable = CreateTable(tableWidth, detailHeight, detailIndent); detailTable.CanGrow = true; var detailRow = new XRTableRow { HeightF = detailHeight, CanGrow = true }; - var footerTable = CreateTable(pageWidth, 24); + var footerTable = CreateTable(tableWidth, 24, detailIndent); var footerRow = new XRTableRow { HeightF = 24 }; for (var i = 0; i < fields.Count; i++) { var field = fields[i]; - headerRow.Cells.Add(CreateHeaderCell(field, widths[i], localizer)); - detailRow.Cells.Add(CreateDetailCell(field, widths[i], imageBaseUrl, detailHeight)); - footerRow.Cells.Add(CreateFooterCell(field, widths[i])); + headerRow.Cells.Add(CreateHeaderCell(field, headerWidths[i], localizer)); + detailRow.Cells.Add(CreateDetailCell(field, detailWidths[i], imageBaseUrl, detailHeight)); + footerRow.Cells.Add(CreateFooterCell(field, detailWidths[i])); } headerTable.Rows.Add(headerRow); @@ -331,11 +382,99 @@ public class DynamicGridReport : XtraReport } } - private static XRTable CreateTable(float width, float height) + private static List CreateGroupHeaderBands( + IReadOnlyList groupFields, + IStringLocalizer localizer, + float pageWidth) + { + var bands = new List(); + for (var i = 0; i < groupFields.Count; i++) + { + var groupField = groupFields[i]; + var level = groupFields.Count - i - 1; + var band = new GroupHeaderBand + { + HeightF = 26, + KeepTogether = true, + RepeatEveryPage = true, + Level = level, + BackColor = System.Drawing.Color.FromArgb(248, 250, 252) + }; + band.GroupFields.Add(new GroupField(groupField.BindingName, XRColumnSortOrder.Ascending)); + + var indent = GetGroupHeaderIndent(i, pageWidth); + var caption = DynamicReportImageHelper.GetLocalizedFieldCaption(groupField.Field, localizer); + var availableWidth = pageWidth - indent; + var captionWidth = Math.Clamp(caption.Length * 6F + 28F, 90F, Math.Min(220F, availableWidth / 2)); + + var captionLabel = new XRLabel + { + Text = $"{caption}:", + BoundsF = new System.Drawing.RectangleF(indent, 3, captionWidth, 20), + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), + Padding = new PaddingInfo(6, 2, 0, 0), + TextAlignment = TextAlignment.MiddleLeft, + BackColor = System.Drawing.Color.FromArgb(226, 232, 240), + Borders = BorderSide.Left | BorderSide.Top | BorderSide.Bottom, + BorderColor = TableBorderColor, + BorderWidth = 0.25F + }; + var valueLabel = new XRLabel + { + BoundsF = new System.Drawing.RectangleF(indent + captionWidth, 3, availableWidth - captionWidth, 20), + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), + Padding = new PaddingInfo(6, 6, 0, 0), + TextAlignment = TextAlignment.MiddleLeft, + BackColor = System.Drawing.Color.FromArgb(241, 245, 249), + Borders = BorderSide.All, + BorderColor = TableBorderColor, + BorderWidth = 0.25F, + CanGrow = true, + Multiline = true + }; + valueLabel.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Text", + DynamicReportImageHelper.CreateFormatValueExpression( + $"[{groupField.BindingName}]", + groupField.Field, + groupField.EditorType))); + + band.Controls.AddRange([captionLabel, valueLabel]); + bands.Add(band); + } + + return bands; + } + + private static float GetGroupHeaderIndent(int groupIndex, float pageWidth) + { + return Math.Min(groupIndex * GroupIndentWidth, pageWidth * 0.25F); + } + + private static float GetGroupDetailIndent(int groupCount, float pageWidth) + { + return Math.Min(groupCount * GroupIndentWidth, pageWidth * 0.25F); + } + + private static float[] CreateIndentedHeaderWidths(float[] detailWidths, float detailIndent) + { + if (detailWidths.Length == 0 || detailIndent <= 0) + { + return detailWidths; + } + + var headerWidths = detailWidths.ToArray(); + headerWidths[0] += detailIndent; + + return headerWidths; + } + + private static XRTable CreateTable(float width, float height, float left = 0) { return new XRTable { - BoundsF = new System.Drawing.RectangleF(0, 0, width, height), + BoundsF = new System.Drawing.RectangleF(left, 0, width, height), Borders = BorderSide.All, BorderColor = TableBorderColor, BorderWidth = 0.25F @@ -387,7 +526,7 @@ public class DynamicGridReport : XtraReport { WidthF = width, Weight = width, - Font = new DXFont("Segoe UI Emoji", 8F), + Font = new DXFont("Arial", 8F), Padding = new PaddingInfo(4, 4, 2, 2), TextAlignment = DynamicReportImageHelper.ToTextAlignment(field.Alignment, field.SourceDbType), Multiline = true, @@ -475,18 +614,6 @@ public class DynamicGridReport : XtraReport }; } - private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) - { - var raw = fields.Select(x => (float)Math.Max(x.Width ?? 90, 60)).ToArray(); - var total = raw.Sum(); - if (total <= 0) - { - return fields.Select(_ => pageWidth / fields.Count).ToArray(); - } - - return raw.Select(x => pageWidth * x / total).ToArray(); - } - private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) { var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).ToArray(); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs index 1b5ccc8..69af479 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs @@ -55,7 +55,7 @@ public partial class DynamicReport : XtraReport HorizontalMargin, VerticalMargin, VerticalMargin); - Font = new DXFont("Arial", 8.25F); + Font = new DXFont("Arial", 8F); TopMargin.HeightF = Margins.Top; BottomMargin.HeightF = Margins.Bottom; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs index 90a41c8..25e5992 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Sozsoft.Platform; using Sozsoft.Platform.Entities; +using Sozsoft.Platform.ListForms; namespace Sozsoft.Reports.PredefinedReports; @@ -32,6 +34,51 @@ internal static class DynamicReportGridStateHelper .ToList(); } + public static List GetGroupedFields( + IReadOnlyList availableFields, + string gridStateJson, + string groupParameter) + { + var groupedFields = new List(); + var groupedFieldNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var field in GetGroupFieldsFromParameter(availableFields, groupParameter)) + { + AddGroupedField(field); + } + + if (TryGetColumnStates(gridStateJson, out var statesByField)) + { + foreach (var field in availableFields.Where(field => + statesByField.TryGetValue(field.FieldName, out var state) && + state.GroupIndex.HasValue) + .OrderBy(field => statesByField[field.FieldName].GroupIndex) + .ThenBy(field => statesByField[field.FieldName].VisibleIndex)) + { + AddGroupedField(field); + } + } + + foreach (var field in availableFields + .Select(field => new { Field = field, GroupIndex = GetConfiguredGroupIndex(field) }) + .Where(x => x.GroupIndex.HasValue) + .OrderBy(x => x.GroupIndex) + .Select(x => x.Field)) + { + AddGroupedField(field); + } + + return groupedFields; + + void AddGroupedField(ListFormField field) + { + if (!string.IsNullOrWhiteSpace(field?.FieldName) && groupedFieldNames.Add(field.FieldName)) + { + groupedFields.Add(field); + } + } + } + private static string GetStateStorageKey(ListForm listForm) { if (!string.IsNullOrWhiteSpace(listForm.StateStoringJson)) @@ -100,10 +147,14 @@ internal static class DynamicReportGridStateHelper visibleIndexProperty.TryGetInt32(out var value) ? value : states.Count; + var groupIndex = TryGetProperty(column, "groupIndex", out var groupIndexProperty) && + groupIndexProperty.TryGetInt32(out var groupValue) + ? groupValue + : (int?)null; if (!string.IsNullOrWhiteSpace(fieldName)) { - states.Add(new GridColumnState(fieldName, visible, visibleIndex)); + states.Add(new GridColumnState(fieldName, visible, visibleIndex, groupIndex)); } if (TryGetProperty(column, "columns", out var childColumns) && @@ -131,6 +182,55 @@ internal static class DynamicReportGridStateHelper return null; } + private static List GetGroupFieldsFromParameter( + IReadOnlyList availableFields, + string groupParameter) + { + var result = new List(); + if (string.IsNullOrWhiteSpace(groupParameter)) + { + return result; + } + + var fieldsByName = availableFields + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName)) + .ToDictionary(x => x.FieldName, StringComparer.OrdinalIgnoreCase); + + foreach (var group in groupParameter.Split( + PlatformConsts.MultiValueDelimiter, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var parts = group.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length > 0 && fieldsByName.TryGetValue(parts[0], out var field)) + { + result.Add(field); + } + } + + return result; + } + + private static int? GetConfiguredGroupIndex(ListFormField field) + { + if (string.IsNullOrWhiteSpace(field.GroupingJson)) + { + return null; + } + + try + { + var grouping = JsonSerializer.Deserialize(field.GroupingJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + return grouping?.GroupIndex; + } + catch (JsonException) + { + return null; + } + } + private static bool TryGetProperty(JsonElement element, string name, out JsonElement value) { if (element.ValueKind == JsonValueKind.Object) @@ -149,5 +249,5 @@ internal static class DynamicReportGridStateHelper return false; } - private sealed record GridColumnState(string FieldName, bool Visible, int VisibleIndex); + private sealed record GridColumnState(string FieldName, bool Visible, int VisibleIndex, int? GroupIndex); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs index 6a4656f..a4b11e3 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -52,7 +53,7 @@ internal static class DynamicReportSqlBuilder var lookupSelectParts = columns .Select(column => CreateLookupSelectPart(column, defaultValueHelper, dataSourceType)) - .Where(part => !string.IsNullOrWhiteSpace(part)) + .Where(part => part != null && !string.IsNullOrWhiteSpace(part.SelectSql)) .ToList(); if (lookupSelectParts.Count == 0) @@ -68,10 +69,19 @@ internal static class DynamicReportSqlBuilder normalizedSql = OrderByRegex.Replace(normalizedSql, string.Empty).Trim(); } - return $"SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts)} FROM ({normalizedSql}) AS {sourceAlias}{orderBy}"; + var commonTableExpressions = lookupSelectParts + .Select(part => part.CommonTableExpression) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct() + .ToList(); + var withClause = commonTableExpressions.Count > 0 + ? $"WITH {string.Join(", ", commonTableExpressions)} " + : string.Empty; + + return $"{withClause}SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts.Select(x => x.SelectSql))} FROM ({normalizedSql}) AS {sourceAlias}{orderBy}"; } - private static string CreateLookupSelectPart( + private static LookupSelectPart CreateLookupSelectPart( IReportLookupColumn column, DefaultValueHelper defaultValueHelper, DataSourceTypeEnum dataSourceType) @@ -89,7 +99,7 @@ internal static class DynamicReportSqlBuilder return null; } - private static string CreateStaticLookupSelectPart( + private static LookupSelectPart CreateStaticLookupSelectPart( IReportLookupColumn column, DataSourceTypeEnum dataSourceType) { @@ -102,10 +112,11 @@ internal static class DynamicReportSqlBuilder var cases = column.StaticLookupValues .Select(item => $"WHEN {DynamicReportImageHelper.ToSqlLiteral(item.Key, dataSourceType)} THEN {DynamicReportImageHelper.ToSqlLiteral(item.Value, dataSourceType)}"); - return $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}"; + return new LookupSelectPart( + $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}"); } - private static string CreateQueryLookupSelectPart( + private static LookupSelectPart CreateQueryLookupSelectPart( IReportLookupColumn column, DefaultValueHelper defaultValueHelper, DataSourceTypeEnum dataSourceType) @@ -121,6 +132,7 @@ internal static class DynamicReportSqlBuilder return CreateQueryMultiLookupSelectPart(column, lookupQuery, dataSourceType); } + var (commonTableExpression, lookupSelectQuery) = SplitCommonTableExpression(lookupQuery); var lookupAlias = QuoteSqlIdentifier(LookupAlias); var valueExpr = QuoteSqlIdentifier(column.Lookup.ValueExpr ?? "Key"); var displayExpr = QuoteSqlIdentifier(column.Lookup.DisplayExpr ?? "Name"); @@ -128,13 +140,13 @@ internal static class DynamicReportSqlBuilder var resultAlias = QuoteSqlIdentifier(column.BindingName); var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql - ? $"SELECT TOP 1 {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}" - : $"SELECT {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1"; + ? $"SELECT TOP 1 {lookupAlias}.{displayExpr} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}" + : $"SELECT {lookupAlias}.{displayExpr} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1"; - return $"({scalarQuery}) AS {resultAlias}"; + return new LookupSelectPart($"({scalarQuery}) AS {resultAlias}", commonTableExpression); } - private static string CreateStaticMultiLookupSelectPart( + private static LookupSelectPart CreateStaticMultiLookupSelectPart( IReportLookupColumn column, DataSourceTypeEnum dataSourceType) { @@ -147,27 +159,28 @@ internal static class DynamicReportSqlBuilder if (dataSourceType == DataSourceTypeEnum.Mssql) { - return $""" + return new LookupSelectPart($""" (SELECT STRING_AGG(CAST({LookupAliasForValues()}.DisplayValue AS NVARCHAR(MAX)), N', ') FROM (VALUES {values}) AS {LookupAliasForValues()}(ValueKey, DisplayValue) INNER JOIN STRING_SPLIT(CAST({sourceField} AS NVARCHAR(MAX)), N'{PlatformConsts.MultiValueDelimiter}') AS SplitValues ON CAST({LookupAliasForValues()}.ValueKey AS NVARCHAR(4000)) = LTRIM(RTRIM(SplitValues.value))) AS {resultAlias} - """.ReplaceLineEndings(" ").Trim(); + """.ReplaceLineEndings(" ").Trim()); } - return $""" + return new LookupSelectPart($""" (SELECT string_agg(CAST({LookupAliasForValues()}."DisplayValue" AS TEXT), ', ') FROM (VALUES {values}) AS {LookupAliasForValues()}("ValueKey", "DisplayValue") INNER JOIN unnest(string_to_array(CAST({sourceField} AS TEXT), '{PlatformConsts.MultiValueDelimiter}')) AS SplitValues(value) ON CAST({LookupAliasForValues()}."ValueKey" AS TEXT) = btrim(SplitValues.value)) AS {resultAlias} - """.ReplaceLineEndings(" ").Trim(); + """.ReplaceLineEndings(" ").Trim()); } - private static string CreateQueryMultiLookupSelectPart( + private static LookupSelectPart CreateQueryMultiLookupSelectPart( IReportLookupColumn column, string lookupQuery, DataSourceTypeEnum dataSourceType) { + var (commonTableExpression, lookupSelectQuery) = SplitCommonTableExpression(lookupQuery); var lookupAlias = QuoteSqlIdentifier(LookupAlias); var valueExpr = QuoteSqlIdentifier(column.Lookup.ValueExpr ?? "Key"); var displayExpr = QuoteSqlIdentifier(column.Lookup.DisplayExpr ?? "Name"); @@ -177,18 +190,92 @@ internal static class DynamicReportSqlBuilder var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql ? $""" SELECT STRING_AGG(CAST({lookupAlias}.{displayExpr} AS NVARCHAR(MAX)), N', ') - FROM ({lookupQuery}) AS {lookupAlias} + FROM ({lookupSelectQuery}) AS {lookupAlias} INNER JOIN STRING_SPLIT(CAST({sourceField} AS NVARCHAR(MAX)), N'{PlatformConsts.MultiValueDelimiter}') AS SplitValues ON CAST({lookupAlias}.{valueExpr} AS NVARCHAR(4000)) = LTRIM(RTRIM(SplitValues.value)) """.ReplaceLineEndings(" ").Trim() : $""" SELECT string_agg(CAST({lookupAlias}.{displayExpr} AS TEXT), ', ') - FROM ({lookupQuery}) AS {lookupAlias} + FROM ({lookupSelectQuery}) AS {lookupAlias} INNER JOIN unnest(string_to_array(CAST({sourceField} AS TEXT), '{PlatformConsts.MultiValueDelimiter}')) AS SplitValues(value) ON CAST({lookupAlias}.{valueExpr} AS TEXT) = btrim(SplitValues.value) """.ReplaceLineEndings(" ").Trim(); - return $"({scalarQuery}) AS {resultAlias}"; + return new LookupSelectPart($"({scalarQuery}) AS {resultAlias}", commonTableExpression); + } + + private static (string CommonTableExpression, string SelectQuery) SplitCommonTableExpression(string query) + { + var normalized = query.Trim().TrimStart(';').TrimStart(); + if (!normalized.StartsWith("WITH ", System.StringComparison.OrdinalIgnoreCase) && + !normalized.StartsWith("WITH\t", System.StringComparison.OrdinalIgnoreCase) && + !normalized.StartsWith("WITH\r", System.StringComparison.OrdinalIgnoreCase) && + !normalized.StartsWith("WITH\n", System.StringComparison.OrdinalIgnoreCase)) + { + return (null, normalized); + } + + var depth = 0; + var quote = '\0'; + for (var index = 4; index < normalized.Length; index++) + { + var current = normalized[index]; + if (quote != '\0') + { + if (current == quote) + { + if (index + 1 < normalized.Length && normalized[index + 1] == quote) + { + index++; + } + else + { + quote = '\0'; + } + } + + continue; + } + + if (current is '\'' or '"') + { + quote = current; + continue; + } + + if (current == '(') + { + depth++; + continue; + } + + if (current == ')') + { + depth--; + continue; + } + + if (depth == 0 && IsKeywordAt(normalized, index, "SELECT")) + { + return (normalized[4..index].Trim(), normalized[index..].Trim()); + } + } + + return (null, normalized); + } + + private static bool IsKeywordAt(string value, int index, string keyword) + { + if (index + keyword.Length > value.Length || + !value.AsSpan(index, keyword.Length).Equals(keyword, System.StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var hasBoundaryBefore = index == 0 || !char.IsLetterOrDigit(value[index - 1]) && value[index - 1] != '_'; + var after = index + keyword.Length; + var hasBoundaryAfter = after == value.Length || !char.IsLetterOrDigit(value[after]) && value[after] != '_'; + return hasBoundaryBefore && hasBoundaryAfter; } private static string LookupAliasForValues() => "LookupValues"; @@ -227,4 +314,6 @@ internal static class DynamicReportSqlBuilder return $"\"{identifier.Replace("\"", "\"\"")}\""; } + private sealed record LookupSelectPart(string SelectSql, string CommonTableExpression = null); + } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs index 7ac7ab5..f876812 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -183,7 +183,7 @@ public class DynamicTreeReport : XtraReport var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth; - Font = new DXFont("Arial", 8.25F); + Font = new DXFont("Arial", 8F); } private static System.Drawing.Printing.Margins CreateReportMargins( @@ -550,7 +550,7 @@ public class DynamicTreeReport : XtraReport { WidthF = width, Weight = width, - Font = new DXFont("Segoe UI Emoji", 8F), + Font = new DXFont("Arial", 8F), Padding = new PaddingInfo(4, 4, 2, 2), TextAlignment = isTreeColumn ? TextAlignment.MiddleLeft diff --git a/ui/public/version.json b/ui/public/version.json index 46c0ed2..b7d3314 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -1,6 +1,19 @@ { - "commit": "771ea95", + "commit": "e7670c6", "releases": [ + { + "version": "1.1.05", + "buildDate": "2026-06-27", + "commit": "352cf047b0af1b8aad53b632a41161efcc6cecdf", + "changeLog": [ + "- Ai için Ollama kurulumu yapıldı. Ayrıca Gemma LLM ile çalıştırıldı.", + "- DynamicReport eklendi. ReportDesigner ile çalışan bir rapor tasarlandı.", + "- DynamicGridReport eklendi. Grid üzerinde otomatik rapor alınabiliyor.", + "- DynamicTreeReport eklendi. Tree üzerinden otomatik rapor alınabiliyor.", + "- DynamicFormReport eklendi. Single ekranlarda otomatik rapor eklendi.", + "- Demo sayfası menüden ayrıldı." + ] + }, { "version": "1.1.04", "buildDate": "2026-06-04",