From 6884812484675573bc43049e9f37373bead7c84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:04:55 +0300 Subject: [PATCH] =?UTF-8?q?Devexpress=20Grid=20Raporu=20otomatik=20olu?= =?UTF-8?q?=C5=9Fturma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Seeds/ListFormSeeder_Administration.cs | 11 + .../Seeds/TenantData.json | 13 +- .../CustomWebDocumentViewerController.cs | 6 +- .../DynamicListFormReport.cs | 480 ++++++++++++++++++ .../CustomReportStorageWebExtension.cs | 75 ++- ui/src/routes/route.constant.ts | 4 +- .../views/report/DevexpressReportViewer.tsx | 43 +- 7 files changed, 594 insertions(+), 38 deletions(-) create mode 100644 api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicListFormReport.cs diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index f0f4094..7220b59 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -1586,6 +1586,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep AuthName = listFormName, Url=$"/admin/reports/TemplateReport/view?id=@Id", IsVisible = true, + VisibleExpression = "((e)=> e.row.data.Description !== 'Liste Raporları')" }, new() { Hint = "App.Platform.Design", @@ -1594,6 +1595,16 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep AuthName = listFormName + ".Update", Url=$"/admin/reports/TemplateReport/design?id=@Id", IsVisible = true, + VisibleExpression = "((e)=> e.row.data.Description !== 'Liste Raporları')" + }, + new() { + Hint = "App.Platform.View", + Text ="App.Platform.View", + UrlTarget="_blank", + AuthName = listFormName, + Url=$"/admin/reports/DynamicListFormReport/view?listFormCode=@Name", + IsVisible = true, + VisibleExpression = "((e)=> e.row.data.Description !== 'Liste Raporları')" }, }), } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index 3a8480e..fc10183 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -1373,8 +1373,8 @@ "icon": "✍️" }, { - "name": "Sözleşmeler", - "description": "Tedarikçiler ile ilgili sözleşme raporları", + "name": "Listeler", + "description": "Tüm listelerin raporları", "icon": "📜" } ], @@ -1384,7 +1384,14 @@ "description": "Standard Taahhütname", "categoryName": "Taahhütnameler", "htmlContent": "

Taahhütname

Bu belge, tarafların belirli yükümlülüklerini ve taahhütlerini belirtir.

", - "status": "Aktif" + "status": "active" + }, + { + "name": "App.Definitions.AiBot", + "description": "Liste Raporları", + "categoryName": "Listeler", + "htmlContent": "

Liste Raporları.

", + "status": "active" } ], "WorkHours": [ diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/CustomWebDocumentViewerController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/CustomWebDocumentViewerController.cs index e210f04..de6e35c 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/CustomWebDocumentViewerController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/CustomWebDocumentViewerController.cs @@ -1,14 +1,16 @@ -using DevExpress.AspNetCore.Reporting.WebDocumentViewer; +using DevExpress.AspNetCore.Reporting.WebDocumentViewer; using DevExpress.AspNetCore.Reporting.WebDocumentViewer.Native.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Sozsoft.Platform.Controllers; [Route("DXXRDV")] +[Authorize] [ApiExplorerSettings(IgnoreApi = true)] public class CustomWebDocumentViewerController : WebDocumentViewerController { - public CustomWebDocumentViewerController(IWebDocumentViewerMvcControllerService controllerService) + public CustomWebDocumentViewerController(IWebDocumentViewerMvcControllerService controllerService) : base(controllerService) { } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicListFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicListFormReport.cs new file mode 100644 index 0000000..aa79ce9 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicListFormReport.cs @@ -0,0 +1,480 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using DevExpress.DataAccess.ConnectionParameters; +using DevExpress.DataAccess.Sql; +using DevExpress.Drawing; +using DevExpress.XtraPrinting; +using DevExpress.XtraReports.UI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Primitives; +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 DynamicListFormReport : XtraReport +{ + public const string ReportName = nameof(DynamicListFormReport); + private const string QueryName = "DynamicList"; + private const int A4PortraitWidth = 827; + private const int HorizontalMargin = 35; + private const int LandscapeColumnThreshold = 5; + private static IServiceProvider _serviceProvider; + + public static void SetServiceProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public static DynamicListFormReport Create(IDictionary parameters) + { + var report = new DynamicListFormReport(); + report.Build(parameters); + return report; + } + + public DynamicListFormReport() + { + } + + private void Build(IDictionary parameters) + { + if (_serviceProvider == null) + { + throw new InvalidOperationException("DynamicListFormReport 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 localizer = scope.ServiceProvider.GetRequiredService>(); + + if (!authManager.CanAccess(listFormCode).GetAwaiter().GetResult()) + { + throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'."); + } + + var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult(); + var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult() + .Where(x => x.CanRead) + .OrderBy(x => x.ListOrderNo ?? int.MaxValue) + .ThenBy(x => x.CaptionName) + .ToList(); + var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult(); + var (_, connectionString, dataSourceType) = dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode).GetAwaiter().GetResult(); + + var queryParameters = ToStringValues(parameters); + 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 visibleFields = fields + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") + .Where(x => x.IsActive != false && x.Visible == true) + .Where(x => selectQueryManager.SelectFields.Any(s => s.FieldName == 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 => selectQueryManager.SelectFields.Any(s => s.FieldName == x.FieldName)) + .Take(8) + .ToList(); + } + + var reportColumns = visibleFields + .Select(field => + { + var selectField = selectQueryManager.SelectFields.First(s => s.FieldName == field.FieldName); + return new ReportColumn(field, GetReportFieldName(selectField)); + }) + .Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined") + .ToList(); + + ConfigurePage(reportColumns); + ConfigureDataSource( + ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters), + connectionString, + dataSourceType); + BuildLayout(listForm, reportColumns, localizer); + RequestParameters = false; + } + + private void ConfigurePage(IReadOnlyCollection visibleFields) + { + Margins = new System.Drawing.Printing.Margins(HorizontalMargin, HorizontalMargin, 35, 35); + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; + var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; + var requestedContentWidth = visibleFields.Sum(x => Math.Max(x.Field.Width ?? 90, 60)); + Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth; + Font = new DXFont("Arial", 8.25F); + } + + private void ConfigureDataSource(string selectQuery, string connectionString, DataSourceTypeEnum dataSourceType) + { + var dataSource = new SqlDataSource(new CustomStringConnectionParameters(ToDevExpressConnectionString(connectionString, dataSourceType))); + dataSource.Queries.Add(new CustomSqlQuery(QueryName, selectQuery)); + ComponentStorage.Add(dataSource); + DataSource = dataSource; + DataMember = QueryName; + } + + private static string ToDevExpressConnectionString(string connectionString, DataSourceTypeEnum dataSourceType) + { + if (string.IsNullOrWhiteSpace(connectionString) || + connectionString.Contains("XpoProvider=", StringComparison.OrdinalIgnoreCase)) + { + return connectionString; + } + + var provider = dataSourceType switch + { + DataSourceTypeEnum.Mssql => "MSSqlServer", + DataSourceTypeEnum.Postgresql => "Postgres", + _ => null + }; + + return string.IsNullOrWhiteSpace(provider) + ? connectionString + : $"XpoProvider={provider};{connectionString}"; + } + + private static string ApplyQueryParameters(string sql, Dictionary parameters) + { + if (string.IsNullOrWhiteSpace(sql) || parameters == null || parameters.Count == 0) + { + return sql; + } + + foreach (var parameter in parameters.OrderByDescending(x => x.Key.Length)) + { + var key = parameter.Key.StartsWith("@") ? parameter.Key : "@" + parameter.Key; + sql = Regex.Replace( + sql, + Regex.Escape(key) + @"\b", + ToSqlLiteral(parameter.Value), + RegexOptions.CultureInvariant); + } + + return sql; + } + + private static string ToSqlLiteral(object value) + { + if (value == null || value == DBNull.Value) + { + return "NULL"; + } + + return value switch + { + string text => $"'{text.Replace("'", "''")}'", + Guid guid => $"'{guid}'", + DateTime date => $"'{date:yyyy-MM-dd HH:mm:ss.fff}'", + DateTimeOffset date => $"'{date:yyyy-MM-dd HH:mm:ss.fff zzz}'", + bool boolean => boolean ? "1" : "0", + byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal + => Convert.ToString(value, CultureInfo.InvariantCulture), + _ => $"'{value.ToString()?.Replace("'", "''")}'" + }; + } + + private void BuildLayout( + ListForm listForm, + IReadOnlyList fields, + IStringLocalizer localizer) + { + Bands.Clear(); + + var topMargin = new TopMarginBand { HeightF = Margins.Top }; + var bottomMargin = new BottomMarginBand { HeightF = 32 }; + var reportHeader = new ReportHeaderBand { HeightF = 42 }; + var pageHeader = new PageHeaderBand { HeightF = 28 }; + var detail = new DetailBand { HeightF = 24 }; + 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 = GetLocalizedReportTitle(listForm, localizer), + BoundsF = new System.Drawing.RectangleF(0, 0, pageWidth, 30), + Font = new DXFont("Arial", 14F, DXFontStyle.Bold), + TextAlignment = TextAlignment.MiddleLeft + }); + + var generatedAt = new XRPageInfo + { + PageInfo = PageInfo.DateTime, + TextFormatString = "{0:dd.MM.yyyy HH:mm}", + BoundsF = new System.Drawing.RectangleF(0, 30, pageWidth / 2, 12), + Font = new DXFont("Arial", 7.5F), + TextAlignment = TextAlignment.MiddleLeft + }; + reportHeader.Controls.Add(generatedAt); + + var pageInfo = 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 + }; + bottomMargin.Controls.Add(pageInfo); + + var widths = CalculateWidths(fields, pageWidth); + var headerTable = CreateTable(pageWidth, 28); + var headerRow = new XRTableRow(); + var detailTable = CreateTable(pageWidth, 24); + var detailRow = new XRTableRow(); + var footerTable = CreateTable(pageWidth, 24); + var footerRow = new XRTableRow(); + + 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])); + 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, + BorderWidth = 0.5F + }; + } + + private static XRTableCell CreateHeaderCell( + ReportColumn column, + float width, + IStringLocalizer localizer) + { + var field = column.Field; + return new XRTableCell + { + Text = GetLocalizedFieldCaption(field, localizer), + WidthF = 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 string GetLocalizedReportTitle(ListForm listForm, IStringLocalizer localizer) + { + var title = string.IsNullOrWhiteSpace(listForm.Title) ? listForm.Name : listForm.Title; + return Localize(localizer, title, listForm.ListFormCode); + } + + private static string GetLocalizedFieldCaption(ListFormField field, IStringLocalizer localizer) + { + var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName; + return Localize(localizer, caption, field.FieldName); + } + + private static string Localize(IStringLocalizer localizer, string key, string fallback) + { + if (string.IsNullOrWhiteSpace(key)) + { + return fallback; + } + + var localized = localizer[key]; + return localized.ResourceNotFound || string.IsNullOrWhiteSpace(localized.Value) + ? key + : localized.Value; + } + + private static XRTableCell CreateDetailCell(ReportColumn column, float width) + { + var field = column.Field; + var cell = new XRTableCell + { + WidthF = width, + Font = new DXFont("Arial", 8F), + Padding = new PaddingInfo(4, 4, 2, 2), + TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType), + Multiline = false, + CanGrow = false + }; + cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", $"[{column.BindingName}]")); + return cell; + } + + private static XRTableCell CreateFooterCell(ReportColumn column, float width) + { + var field = column.Field; + var summaryType = GetSummaryType(field.TotalSummaryJson); + var cell = new XRTableCell + { + WidthF = width, + Font = new DXFont("Arial", 8F, DXFontStyle.Bold), + Padding = new PaddingInfo(4, 4, 2, 2), + TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType) + }; + + if (summaryType == null) + { + return cell; + } + + cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", $"sumSum([{column.BindingName}])")); + 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 => (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 => (float)Math.Max(x.Field.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 bool HasAnySummary(IEnumerable fields) + { + return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null); + } + + private static string GetReportFieldName(SelectField selectField) + { + if (selectField?.JoinOptions != null && !string.IsNullOrWhiteSpace(selectField.JoinOptions.FieldNameAlias)) + { + return selectField.JoinOptions.FieldNameAlias; + } + + return selectField?.FieldName; + } + + private sealed record ReportColumn(ListFormField Field, string BindingName); + + 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 TextAlignment ToTextAlignment(string alignment, DbType dbType) + { + return alignment?.ToLowerInvariant() switch + { + "center" => TextAlignment.MiddleCenter, + "right" => TextAlignment.MiddleRight, + "left" => TextAlignment.MiddleLeft, + _ => IsNumeric(dbType) ? TextAlignment.MiddleRight : TextAlignment.MiddleLeft + }; + } + + private static bool IsNumeric(DbType dbType) + { + return dbType is DbType.Currency or DbType.Decimal or DbType.Double or DbType.Int16 or DbType.Int32 + or DbType.Int64 or DbType.Single or DbType.UInt16 or DbType.UInt32 or DbType.UInt64; + } + + private static string GetParameter(IDictionary parameters, string name) + { + return parameters.TryGetValue(name, out var value) ? value : null; + } + + private static Dictionary ToStringValues(IDictionary parameters) + { + return parameters.ToDictionary(x => x.Key, x => new StringValues(x.Value)); + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index b929a28..90acbf2 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -21,6 +21,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // TemplateReport için ServiceProvider'ı set et Sozsoft.Reports.PredefinedReports.TemplateReport.SetServiceProvider(serviceProvider); + Sozsoft.Reports.PredefinedReports.DynamicListFormReport.SetServiceProvider(serviceProvider); if (!Directory.Exists(reportDirectory)) { @@ -34,6 +35,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // TemplateReport için ServiceProvider'ı set et Sozsoft.Reports.PredefinedReports.TemplateReport.SetServiceProvider(serviceProvider); + Sozsoft.Reports.PredefinedReports.DynamicListFormReport.SetServiceProvider(serviceProvider); if (!Directory.Exists(reportDirectory)) { @@ -65,8 +67,8 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // Implement your own logic to prohibit URLs that contain spaces or other specific characters. // Return **true** if no validation is required. - // Query string varsa sadece rapor adını kontrol et - string reportName = url.Contains("?") ? url.Split('?')[0] : url; + // Query string encoded gelse bile sadece rapor adını kontrol et. + var (reportName, _) = ParseReportUrl(url); return Path.GetFileName(reportName) == reportName; } @@ -78,26 +80,14 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens // if the parameters are included in the report URL's query string. try { - // URL'den rapor adını ve parametreleri ayır - string reportName = url; - Dictionary parameters = new Dictionary(); + var (reportName, parameters) = ParseReportUrl(url); - if (url.Contains("?")) + if (reportName == Sozsoft.Reports.PredefinedReports.DynamicListFormReport.ReportName) { - var parts = url.Split('?'); - reportName = parts[0]; - - // Query string parametrelerini parse et - var queryString = parts[1]; - var paramPairs = queryString.Split('&'); - foreach (var pair in paramPairs) - { - var keyValue = pair.Split('='); - if (keyValue.Length == 2) - { - parameters[keyValue[0]] = Uri.UnescapeDataString(keyValue[1]); - } - } + var report = Sozsoft.Reports.PredefinedReports.DynamicListFormReport.Create(parameters); + using MemoryStream ms = new MemoryStream(); + report.SaveLayoutToXml(ms); + return ms.ToArray(); } // Önce Reports klasöründen yüklemeyi dene @@ -177,6 +167,51 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens throw new FaultException(new FaultReason(string.Format("Could not find report '{0}'.", url)), new FaultCode("Server"), "GetData"); } + private static (string ReportName, Dictionary Parameters) ParseReportUrl(string url) + { + var decodedUrl = DecodeRepeatedly(url); + var reportName = decodedUrl; + var parameters = new Dictionary(); + + var queryIndex = decodedUrl.IndexOf('?'); + if (queryIndex < 0) + { + return (reportName, parameters); + } + + reportName = decodedUrl[..queryIndex]; + var queryString = decodedUrl[(queryIndex + 1)..]; + var paramPairs = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries); + + foreach (var pair in paramPairs) + { + var keyValue = pair.Split('=', 2); + if (keyValue.Length == 2 && !string.IsNullOrWhiteSpace(keyValue[0])) + { + parameters[keyValue[0]] = keyValue[1]; + } + } + + return (reportName, parameters); + } + + private static string DecodeRepeatedly(string value) + { + var decoded = value; + for (var i = 0; i < 3; i++) + { + var next = Uri.UnescapeDataString(decoded); + if (next == decoded) + { + return decoded; + } + + decoded = next; + } + + return decoded; + } + public override Dictionary GetUrls() { // Returns a dictionary that contains the report names (URLs) and display names. diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index 809c3cd..739d754 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -50,8 +50,8 @@ export const ROUTES_ENUM = { dynamicServicesEdit: '/admin/developerkit/dynamic-services/edit/:id', }, reports: { - generator: '/admin/reports/generator', - view: '/admin/reports/:id', + view: '/admin/reports/:id/view', + design: '/admin/reports/:id/design', }, }, admin: { diff --git a/ui/src/views/report/DevexpressReportViewer.tsx b/ui/src/views/report/DevexpressReportViewer.tsx index 17dbefb..7ef5c17 100644 --- a/ui/src/views/report/DevexpressReportViewer.tsx +++ b/ui/src/views/report/DevexpressReportViewer.tsx @@ -1,15 +1,14 @@ -import React, { useMemo, lazy, Suspense, useEffect } from 'react' +import React, { useMemo, useEffect, useState } from 'react' import { Container } from '@/components/shared' import { Helmet } from 'react-helmet' import { useLocalization } from '@/utils/hooks/useLocalization' import { useParams, useLocation } from 'react-router-dom' -import { RequestOptions } from 'devexpress-reporting-react/dx-report-viewer' +import ReportViewer, { + RequestOptions, +} from 'devexpress-reporting-react/dx-report-viewer' +import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native' import { APP_NAME } from '@/constants/app.constant' - -// Lazy load -const ReportViewer = lazy(() => - import('devexpress-reporting-react/dx-report-viewer') -) +import { useStoreState } from '@/store' const loadViewerCss = () => { const styles = [ @@ -31,11 +30,33 @@ const DevexpressReportViewer: React.FC = () => { const { translate } = useLocalization() const { id } = useParams<{ id: string }>() const location = useLocation() + const token = useStoreState((state) => state.auth.session.token) + const [isRequestAuthReady, setIsRequestAuthReady] = useState(false) useEffect(() => { loadViewerCss() }, []) + useEffect(() => { + setIsRequestAuthReady(false) + const authorization = token ? `Bearer ${token}` : undefined + + const ajaxHeaders = ajaxSetup.ajaxSettings.headers ?? {} + + if (authorization) { + ajaxSetup.ajaxSettings.headers = { + ...ajaxHeaders, + Authorization: authorization, + } + setIsRequestAuthReady(true) + return + } + + const { Authorization: _ajaxAuthorization, ...nextAjaxHeaders } = ajaxHeaders + ajaxSetup.ajaxSettings.headers = nextAjaxHeaders + setIsRequestAuthReady(true) + }, [token]) + const reportUrlWithParams = useMemo(() => { if (!id) return '' return location.search ? `${id}${location.search}` : id @@ -51,14 +72,14 @@ const DevexpressReportViewer: React.FC = () => { defaultTitle={APP_NAME} /> - Rapor yükleniyor...}> + {isRequestAuthReady && ( - + )} ) }