From 2118acf6e1fce2f1be0773ec0ac2256f5895cafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 17 Jun 2026 22:55:23 +0300 Subject: [PATCH] =?UTF-8?q?Dynamic=20Tree=20Report=20haz=C4=B1rland=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PredefinedReports/DynamicTreeReport.cs | 790 ++++++++++++++++++ .../CustomReportStorageWebExtension.cs | 17 +- ui/src/views/list/useFilters.tsx | 12 +- ui/src/views/report/reportRouteParams.ts | 5 +- 4 files changed, 818 insertions(+), 6 deletions(-) create mode 100644 api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs new file mode 100644 index 0000000..61ea684 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -0,0 +1,790 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using DevExpress.DataAccess.Json; +using DevExpress.Drawing; +using DevExpress.XtraPrinting; +using DevExpress.XtraReports.UI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Sozsoft.Platform; +using Sozsoft.Platform.DynamicData; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms; +using Sozsoft.Platform.Localization; +using Sozsoft.Platform.Queries; + +namespace Sozsoft.Reports.PredefinedReports; + +public class DynamicTreeReport : XtraReport +{ + public const string ReportName = nameof(DynamicTreeReport); + private const int A4PortraitWidth = 827; + private const int HorizontalMargin = 35; + private const int LandscapeColumnThreshold = 5; + private const string TreeLevelColumn = "__TreeLevel"; + private const string TreeHasChildrenColumn = "__TreeHasChildren"; + private const string TreePrefixColumn = "__TreePrefix"; + private const string TreeDataSourceName = "DynamicTree"; + private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); + private static IServiceProvider _serviceProvider; + + public static void SetServiceProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public static async Task CreateAsync(IDictionary parameters) + { + var report = new DynamicTreeReport(); + await report.BuildAsync(parameters); + return report; + } + + public DynamicTreeReport() + { + } + + private async Task BuildAsync(IDictionary parameters) + { + if (_serviceProvider == null) + { + throw new InvalidOperationException("DynamicTreeReport service provider is not configured."); + } + + if (!parameters.TryGetValue("listFormCode", out var listFormCode) || string.IsNullOrWhiteSpace(listFormCode)) + { + throw new ArgumentException("listFormCode parameter is required."); + } + + using var scope = _serviceProvider.CreateScope(); + var authManager = scope.ServiceProvider.GetRequiredService(); + var listFormManager = scope.ServiceProvider.GetRequiredService(); + var listFormFieldManager = scope.ServiceProvider.GetRequiredService(); + var customizationManager = scope.ServiceProvider.GetRequiredService(); + var dynamicDataManager = scope.ServiceProvider.GetRequiredService(); + var selectQueryManager = scope.ServiceProvider.GetRequiredService(); + var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); + var localizer = scope.ServiceProvider.GetRequiredService>(); + var configuration = scope.ServiceProvider.GetRequiredService(); + + if (!await authManager.CanAccess(listFormCode)) + { + throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'."); + } + + var listForm = await listFormManager.GetUserListForm(listFormCode); + var treeOptions = GetTreeOptions(listForm); + if (string.IsNullOrWhiteSpace(treeOptions?.KeyExpr) || string.IsNullOrWhiteSpace(treeOptions.ParentIdExpr)) + { + throw new InvalidOperationException($"Tree options are not configured for list form '{listFormCode}'."); + } + + var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode)) + .Where(x => x.CanRead) + .OrderBy(x => x.ListOrderNo ?? int.MaxValue) + .ThenBy(x => x.CaptionName) + .ToList(); + var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode); + var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); + + var queryParams = new QueryParameters + { + ListFormCode = listFormCode, + Filter = GetParameter(parameters, "filter"), + Sort = GetParameter(parameters, "sort"), + Group = GetParameter(parameters, "group"), + GroupSummary = GetParameter(parameters, "groupSummary"), + TotalSummary = GetParameter(parameters, "totalSummary"), + Take = 0, + Skip = 0 + }; + + selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams); + var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields); + var editorTypesByField = DynamicReportImageHelper.CreateEditorTypeMap(listForm); + + var visibleFields = fields + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") + .Where(x => x.IsActive != false && x.Visible == true) + .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) + .ToList(); + + if (visibleFields.Count == 0) + { + visibleFields = fields + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") + .Where(x => x.IsActive != false) + .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) + .Take(8) + .ToList(); + } + + 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") + .ToList(); + + var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters( + selectQueryManager.SelectQuery, + selectQueryManager.SelectQueryParameters)); + selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns( + selectQuery, + reportColumns, + defaultValueHelper, + dataSourceType); + + var imageBaseUrl = DynamicReportImageHelper.GetImageBaseUrl(configuration); + 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); + RequestParameters = false; + } + +#pragma warning disable CA1416 + private void ConfigurePage(IReadOnlyCollection visibleFields) + { + Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; + 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); + } + + private static System.Drawing.Printing.Margins CreateReportMargins( + int left, + int right, + int top, + int bottom) + { + return new System.Drawing.Printing.Margins(left, right, top, bottom); + } +#pragma warning restore CA1416 + + private static async Task>> ReadRowsAsync( + IDynamicDataRepository dynamicDataRepository, + string selectQuery, + string connectionString) + { + var rows = await dynamicDataRepository.QueryAsync(selectQuery, connectionString); + return rows + .OfType>() + .Select(row => row.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } + + private static IEnumerable> OrderTreeRows( + IReadOnlyList> rows, + TreeOptionDto treeOptions) + { + var childrenByParent = rows + .GroupBy(row => NormalizeTreeKey(GetRowValue(row, treeOptions.ParentIdExpr))) + .ToDictionary(x => x.Key, x => x.ToList(), StringComparer.OrdinalIgnoreCase); + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var rootKey = NormalizeTreeKey(treeOptions.RootValue); + var roots = childrenByParent.TryGetValue(rootKey, out var configuredRoots) + ? configuredRoots + : rows.Where(row => IsRootValue(GetRowValue(row, treeOptions.ParentIdExpr), treeOptions.RootValue)).ToList(); + + foreach (var row in Traverse(roots, childrenByParent, treeOptions.KeyExpr, 0, visited)) + { + yield return row; + } + + foreach (var row in rows) + { + var key = NormalizeTreeKey(GetRowValue(row, treeOptions.KeyExpr)); + if (visited.Contains(key)) + { + continue; + } + + foreach (var orphan in Traverse([row], childrenByParent, treeOptions.KeyExpr, 0, visited)) + { + yield return orphan; + } + } + } + + private static IEnumerable> Traverse( + IEnumerable> rows, + IReadOnlyDictionary>> childrenByParent, + string keyExpr, + int level, + ISet visited) + { + foreach (var row in rows) + { + var key = NormalizeTreeKey(GetRowValue(row, keyExpr)); + if (!visited.Add(key)) + { + continue; + } + + var hasChildren = childrenByParent.TryGetValue(key, out var children) && children.Count > 0; + row[TreeLevelColumn] = level; + row[TreeHasChildrenColumn] = hasChildren; + row[TreePrefixColumn] = $"{new string(' ', level * 4)}{(hasChildren ? "[-] " : " ")}"; + yield return row; + + if (!hasChildren) + { + continue; + } + + foreach (var child in Traverse(children, childrenByParent, keyExpr, level + 1, visited)) + { + yield return child; + } + } + } + + private static bool IsRootValue(object parentValue, object rootValue) + { + if (rootValue == null) + { + return parentValue == null || + parentValue == DBNull.Value || + string.IsNullOrWhiteSpace(Convert.ToString(parentValue, CultureInfo.InvariantCulture)); + } + + return string.Equals( + NormalizeTreeKey(parentValue), + NormalizeTreeKey(rootValue), + StringComparison.OrdinalIgnoreCase); + } + + private static object GetRowValue(IDictionary row, string fieldName) => + DynamicReportImageHelper.TryGetRowValue(row, fieldName, out var value) ? value : null; + + private static string NormalizeTreeKey(object value) + { + if (value == null || value == DBNull.Value) + { + return string.Empty; + } + + return Convert.ToString(value, CultureInfo.InvariantCulture)?.Trim() ?? string.Empty; + } + + private void ConfigureDataSource( + IEnumerable> rows, + IReadOnlyList columns) + { + var normalizedRows = rows + .Select(row => NormalizeJsonRow(row, columns)) + .ToList(); + if (normalizedRows.Count == 0) + { + normalizedRows.Add(NormalizeJsonRow(new Dictionary(), columns)); + } + + var dataSource = new JsonDataSource + { + Name = TreeDataSourceName, + JsonSource = new CustomJsonSource(JsonSerializer.Serialize(normalizedRows)) + }; + dataSource.Fill(); + ComponentStorage.Add(dataSource); + DataSource = dataSource; + } + + private static Dictionary NormalizeJsonRow( + IDictionary row, + IReadOnlyList columns) + { + var normalized = row.ToDictionary( + x => x.Key, + x => NormalizeJsonValue(x.Value), + StringComparer.OrdinalIgnoreCase); + + foreach (var column in columns) + { + normalized.TryAdd(column.BindingName, null); + } + + normalized.TryAdd(TreeLevelColumn, 0); + normalized.TryAdd(TreeHasChildrenColumn, false); + normalized.TryAdd(TreePrefixColumn, string.Empty); + return normalized; + } + + private static object NormalizeJsonValue(object value) + { + if (value == null || value == DBNull.Value) + { + return null; + } + + if (value is Guid guid) + { + return guid.ToString(); + } + + if (value is DateTime dateTime) + { + return dateTime; + } + + if (value is DateTimeOffset dateTimeOffset) + { + return dateTimeOffset; + } + + if (value is byte[] bytes) + { + return Convert.ToBase64String(bytes); + } + + return value; + } + + private static List ApplyImageCounts( + IReadOnlyList columns, + IReadOnlyList> rows, + string imageBaseUrl) + { + var imageColumns = columns.Where(x => x.IsImage).ToList(); + if (imageColumns.Count == 0) + { + return columns.ToList(); + } + + var imageCounts = imageColumns.ToDictionary(x => x.BindingName, _ => 1, StringComparer.OrdinalIgnoreCase); + foreach (var row in rows) + { + foreach (var column in imageColumns) + { + var count = DynamicReportImageHelper.CountImageSources(GetImageRawValue(row, column), imageBaseUrl); + if (count > imageCounts[column.BindingName]) + { + imageCounts[column.BindingName] = count; + } + } + } + + return columns + .Select(column => column.IsImage + ? column with { ImageCount = imageCounts[column.BindingName] } + : column) + .ToList(); + } + + private static object GetImageRawValue(IDictionary row, ReportColumn column) + { + if (DynamicReportImageHelper.TryGetRowValue(row, column.BindingName, out var value) && + DynamicReportImageHelper.HasValue(value)) + { + return value; + } + + return DynamicReportImageHelper.TryGetRowValue(row, column.Field.FieldName, out value) ? value : null; + } + + private void BuildLayout( + ListForm listForm, + IReadOnlyList fields, + IStringLocalizer localizer, + string imageBaseUrl) + { + Bands.Clear(); + + var topMargin = new TopMarginBand { HeightF = Margins.Top }; + var bottomMargin = new BottomMarginBand { HeightF = 32 }; + var reportHeader = new ReportHeaderBand { HeightF = 32 }; + var pageHeader = new PageHeaderBand { HeightF = 28 }; + var detailHeight = fields.Any(x => x.IsImage) + ? DynamicReportImageHelper.GetGridImageHeight() + : fields.Any(x => x.IsHtml) + ? 48 + : 24; + var detail = new DetailBand { HeightF = detailHeight }; + var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 }; + + Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]); + + var pageWidth = PageWidth - Margins.Left - Margins.Right; + reportHeader.Controls.Add(new XRLabel + { + Text = DynamicReportImageHelper.GetLocalizedReportTitle(listForm, localizer), + BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth, 30), + Font = new DXFont("Arial", 14F, DXFontStyle.Bold), + TextAlignment = TextAlignment.MiddleLeft + }); + + 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), + Font = new DXFont("Arial", 8F), + TextAlignment = TextAlignment.MiddleRight + }); + + var widths = CalculateWidths(fields, pageWidth); + var headerTable = CreateTable(pageWidth, 28); + var headerRow = new XRTableRow { HeightF = 28 }; + var detailTable = CreateTable(pageWidth, detailHeight); + var detailRow = new XRTableRow { HeightF = detailHeight }; + var footerTable = CreateTable(pageWidth, 24); + 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, i == 0)); + footerRow.Cells.Add(CreateFooterCell(field, widths[i])); + } + + headerTable.Rows.Add(headerRow); + detailTable.Rows.Add(detailRow); + footerTable.Rows.Add(footerRow); + + pageHeader.Controls.Add(headerTable); + detail.Controls.Add(detailTable); + if (HasAnySummary(fields)) + { + reportFooter.Controls.Add(footerTable); + } + } + + private static XRTable CreateTable(float width, float height) + { + return new XRTable + { + BoundsF = new System.Drawing.RectangleF(0, 0, width, height), + Borders = BorderSide.All, + BorderColor = TableBorderColor, + BorderWidth = 0.25F + }; + } + + private static XRTableCell CreateHeaderCell( + ReportColumn column, + float width, + IStringLocalizer localizer) + { + return new XRTableCell + { + Text = DynamicReportImageHelper.GetLocalizedFieldCaption(column.Field, localizer), + WidthF = width, + Weight = width, + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), + BackColor = System.Drawing.Color.FromArgb(238, 242, 247), + Padding = new PaddingInfo(4, 4, 2, 2), + TextAlignment = TextAlignment.MiddleCenter, + Multiline = true + }; + } + + private static XRTableCell CreateDetailCell( + ReportColumn column, + float width, + string imageBaseUrl, + float height, + bool isTreeColumn) + { + if (column.IsImage) + { + return DynamicReportImageHelper.CreateGridImageCell( + column.BindingName, + column.Field.FieldName, + width, + imageBaseUrl, + column.ImageCount); + } + + if (column.IsHtml) + { + return CreateHtmlDetailCell(column, width, height); + } + + var cell = new XRTableCell + { + WidthF = width, + Weight = width, + Font = new DXFont("Segoe UI Emoji", 8F), + Padding = new PaddingInfo(4, 4, 2, 2), + TextAlignment = isTreeColumn + ? TextAlignment.MiddleLeft + : DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType), + Multiline = false, + CanGrow = false + }; + + var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", column.Field, column.EditorType); + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Text", + isTreeColumn + ? $"Concat([{TreePrefixColumn}], {valueExpression})" + : valueExpression)); + + if (isTreeColumn) + { + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Font.Bold", + $"[{TreeHasChildrenColumn}]")); + } + + return cell; + } + + private static XRTableCell CreateHtmlDetailCell(ReportColumn column, float width, float height) + { + var cell = new XRTableCell + { + WidthF = width, + Weight = width, + Padding = new PaddingInfo(0, 0, 0, 0), + CanGrow = false + }; + + var richText = new XRRichText + { + BoundsF = new System.Drawing.RectangleF(0, 0, width, height), + Font = new DXFont("Arial", 8F), + Padding = new PaddingInfo(4, 4, 2, 2), + CanGrow = false + }; + richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]")); + cell.Controls.Add(richText); + + return cell; + } + + private static XRTableCell CreateFooterCell(ReportColumn column, float width) + { + var summaryType = GetSummaryType(column.Field.TotalSummaryJson); + var cell = new XRTableCell + { + WidthF = width, + Weight = width, + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), + Padding = new PaddingInfo(4, 4, 2, 2), + TextAlignment = DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType) + }; + + if (summaryType == null) + { + return cell; + } + + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Text", + DynamicReportImageHelper.CreateFormatValueExpression($"sumSum([{column.BindingName}])", column.Field, column.EditorType))); + cell.Summary = new XRSummary + { + Running = SummaryRunning.Report, + Func = summaryType.Value, + IgnoreNullValues = true + }; + + return cell; + } + + private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) + { + var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).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 GetPreferredColumnWidth(ListFormField field) + { + if (field.Width > 0) + { + return Math.Max(field.Width.Value, 60); + } + + var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName; + var name = field.FieldName ?? string.Empty; + var text = $"{caption} {name}".ToLowerInvariant(); + + if (field.SourceDbType == DbType.Boolean) + { + return 75; + } + + if (DynamicReportImageHelper.IsNumeric(field.SourceDbType)) + { + return 70; + } + + if (field.SourceDbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset) + { + return 115; + } + + if (text.Contains("description") || + text.Contains("explanation") || + text.Contains("note") || + text.Contains("comment") || + text.Contains("address") || + text.Contains("content") || + text.Contains("html") || + text.Contains("text")) + { + return 220; + } + + if (text.Contains("name") || + text.Contains("title") || + text.Contains("subject")) + { + return 160; + } + + return Math.Clamp((caption?.Length ?? 0) * 9F + 36F, 85F, 180F); + } + + private static bool HasAnySummary(IEnumerable fields) + { + return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null); + } + + private static float GetImageColumnWidth(ReportColumn column) + { + return DynamicReportImageHelper.GetGridImageColumnWidth(column.ImageCount); + } + + private static ReportColumn CreateReportColumn( + ListFormField field, + SelectField selectField, + DefaultValueHelper defaultValueHelper, + string editorType, + bool isImage, + bool isHtml) + { + var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField); + var lookup = DynamicReportImageHelper.GetLookup(field); + + if (lookup == null || selectField?.JoinOptions != null) + { + return new ReportColumn(field, bindingName, null, null, isImage, isHtml, editorType); + } + + if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup)) + { + return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isImage, isHtml, editorType); + } + + if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData) + { + var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(lookup, defaultValueHelper); + return new ReportColumn( + field, + staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName, + lookup, + staticLookupValues, + isImage, + isHtml, + editorType); + } + + return new ReportColumn(field, bindingName, null, null, isImage, isHtml, editorType); + } + + private static TreeOptionDto GetTreeOptions(ListForm listForm) + { + if (string.IsNullOrWhiteSpace(listForm.TreeOptionJson)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(listForm.TreeOptionJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } + catch (JsonException) + { + return null; + } + } + + private sealed record ReportColumn( + ListFormField Field, + string BindingName, + LookupDto Lookup, + Dictionary StaticLookupValues, + bool IsImage, + bool IsHtml, + string EditorType, + int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn; + + private static SummaryFunc? GetSummaryType(string totalSummaryJson) + { + if (string.IsNullOrWhiteSpace(totalSummaryJson)) + { + return null; + } + + var value = totalSummaryJson.ToLowerInvariant(); + if (value.Contains("\"summarytype\":\"sum\"")) + { + return SummaryFunc.Sum; + } + if (value.Contains("\"summarytype\":\"avg\"")) + { + return SummaryFunc.Avg; + } + if (value.Contains("\"summarytype\":\"min\"")) + { + return SummaryFunc.Min; + } + if (value.Contains("\"summarytype\":\"max\"")) + { + return SummaryFunc.Max; + } + if (value.Contains("\"summarytype\":\"count\"")) + { + return SummaryFunc.Count; + } + + return null; + } + + private static string GetParameter(IDictionary parameters, string name) => + DynamicReportImageHelper.GetParameter(parameters, name); +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index 5b8f913..e9eb924 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -4,7 +4,6 @@ using System.Linq; using System; using System.Reflection; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Sozsoft.Platform.ReportServices; @@ -23,6 +22,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // DynamicReport için ServiceProvider'ı set et Sozsoft.Reports.PredefinedReports.DynamicReport.SetServiceProvider(serviceProvider); Sozsoft.Reports.PredefinedReports.DynamicGridReport.SetServiceProvider(serviceProvider); + Sozsoft.Reports.PredefinedReports.DynamicTreeReport.SetServiceProvider(serviceProvider); Sozsoft.Reports.PredefinedReports.DynamicFormReport.SetServiceProvider(serviceProvider); if (!Directory.Exists(reportDirectory)) @@ -38,6 +38,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // DynamicReport için ServiceProvider'ı set et Sozsoft.Reports.PredefinedReports.DynamicReport.SetServiceProvider(serviceProvider); Sozsoft.Reports.PredefinedReports.DynamicGridReport.SetServiceProvider(serviceProvider); + Sozsoft.Reports.PredefinedReports.DynamicTreeReport.SetServiceProvider(serviceProvider); Sozsoft.Reports.PredefinedReports.DynamicFormReport.SetServiceProvider(serviceProvider); if (!Directory.Exists(reportDirectory)) @@ -168,6 +169,12 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens return SaveReportLayout(report); } + if (IsDynamicTreeReport(reportName)) + { + var report = await Sozsoft.Reports.PredefinedReports.DynamicTreeReport.CreateAsync(parameters); + return SaveReportLayout(report); + } + if (IsDynamicFormReport(reportName)) { var report = await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters); @@ -306,7 +313,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens return; } - if (IsDynamicGridReport(reportName) || IsDynamicFormReport(reportName)) + if (IsDynamicGridReport(reportName) || IsDynamicTreeReport(reportName) || IsDynamicFormReport(reportName)) { if (pathParameters.Length == 1) { @@ -328,6 +335,12 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens string.Equals(reportName, Sozsoft.Reports.PredefinedReports.DynamicGridReport.ReportName, StringComparison.OrdinalIgnoreCase); } + private static bool IsDynamicTreeReport(string reportName) + { + return string.Equals(reportName, "DynamicTree", StringComparison.OrdinalIgnoreCase) || + string.Equals(reportName, Sozsoft.Reports.PredefinedReports.DynamicTreeReport.ReportName, StringComparison.OrdinalIgnoreCase); + } + private static bool IsDynamicFormReport(string reportName) { return string.Equals(reportName, "DynamicForm", StringComparison.OrdinalIgnoreCase) || diff --git a/ui/src/views/list/useFilters.tsx b/ui/src/views/list/useFilters.tsx index c90a1ec..1702cba 100644 --- a/ui/src/views/list/useFilters.tsx +++ b/ui/src/views/list/useFilters.tsx @@ -186,8 +186,12 @@ const getReportSort = (grid: GridInstance) => { .join(MULTIVALUE_DELIMITER) } -const createDynamicGridReportUrl = (grid: GridInstance | undefined, listFormCode: string) => { - const reportUrl = `/admin/reports/DynamicGrid/view/${encodeURIComponent(listFormCode)}` +const createDynamicListReportUrl = ( + grid: GridInstance | undefined, + listFormCode: string, + reportName = 'DynamicGrid', +) => { + const reportUrl = `/admin/reports/${reportName}/view/${encodeURIComponent(listFormCode)}` if (!grid) { return reportUrl } @@ -304,7 +308,9 @@ const useFilters = ({ const menus = [] const openDynamicGridReport = () => { - const reportUrl = createDynamicGridReportUrl(gridRef.current?.instance(), listFormCode) + const grid = gridRef.current?.instance() + const reportName = grid && isTreeList(grid) ? 'DynamicTree' : 'DynamicGrid' + const reportUrl = createDynamicListReportUrl(grid, listFormCode, reportName) window.open(reportUrl, isPwaMode ? '_self' : '_blank') } diff --git a/ui/src/views/report/reportRouteParams.ts b/ui/src/views/report/reportRouteParams.ts index 084eae7..a54f538 100644 --- a/ui/src/views/report/reportRouteParams.ts +++ b/ui/src/views/report/reportRouteParams.ts @@ -5,7 +5,10 @@ export type ReportRouteParams = { } const isDynamicGridReport = (report?: string) => - report === 'DynamicGrid' || report === 'DynamicGridReport' + report === 'DynamicGrid' || + report === 'DynamicGridReport' || + report === 'DynamicTree' || + report === 'DynamicTreeReport' export const normalizeReportRouteParams = ({ report,