From a321e90a53a290e513bd622fc75dd8c4822782cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 17 Jun 2026 21:49:13 +0300 Subject: [PATCH] =?UTF-8?q?Devexpress=20Raporlar=C4=B1n=20Culture=20Uygula?= =?UTF-8?q?mas=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PredefinedReports/DynamicFormReport.cs | 158 +++++---- .../PredefinedReports/DynamicGridReport.cs | 22 +- .../PredefinedReports/DynamicReport.cs | 7 +- .../DynamicReportImageHelper.cs | 300 ++++++++++++++++++ .../CustomReportStorageWebExtension.cs | 13 + .../views/report/DevexpressReportViewer.tsx | 27 +- 6 files changed, 455 insertions(+), 72 deletions(-) diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 3ea5491..49b1183 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -29,6 +29,7 @@ public class DynamicFormReport : XtraReport private const string QueryName = "DynamicForm"; private const int HorizontalMargin = 35; private const float RowHeight = 24; + private const float ColumnGap = 10; private static readonly System.Drawing.Color FieldBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); private static IServiceProvider _serviceProvider; @@ -208,19 +209,28 @@ public class DynamicFormReport : XtraReport y += 26; } - foreach (var field in group.Fields) + var columnCount = GetSafeColumnCount(group.ColumnCount); + var cellWidth = GetCellWidth(pageWidth, columnCount); + foreach (var rowFields in CreateLayoutRows(group.Fields, columnCount)) { - var rowHeight = GetFieldRowHeight(field); - var labelWidth = pageWidth * 0.28F; - detail.Controls.Add(CreateCaptionLabel(field, localizer, 0, y, labelWidth, rowHeight)); - if (field.IsImage) + var rowHeight = rowFields.Max(x => GetFieldRowHeight(x.Field)); + foreach (var item in rowFields) { - detail.Controls.Add(CreateImageValuePanel(field, row, imageBaseUrl, labelWidth, y, pageWidth - labelWidth, rowHeight)); - } - else - { - detail.Controls.Add(CreateValueLabel(field, row, labelWidth, y, pageWidth - labelWidth, rowHeight)); + var x = item.ColumnIndex * (cellWidth + ColumnGap); + var width = cellWidth * item.ColumnSpan + ColumnGap * (item.ColumnSpan - 1); + var labelWidth = Math.Min(width * 0.36F, 110F); + + detail.Controls.Add(CreateCaptionLabel(item.Field, localizer, x, y, labelWidth, rowHeight)); + if (item.Field.IsImage) + { + detail.Controls.Add(CreateImageValuePanel(item.Field, row, imageBaseUrl, x + labelWidth, y, width - labelWidth, rowHeight)); + } + else + { + detail.Controls.Add(CreateValueLabel(item.Field, row, x + labelWidth, y, width - labelWidth, rowHeight)); + } } + y += rowHeight + 3; } @@ -417,19 +427,19 @@ public class DynamicFormReport : XtraReport } var value = Convert.ToString(rawValue, CultureInfo.InvariantCulture); - if (IsBooleanEditor(field) && TryFormatBoolean(value, out var booleanText)) - { - text = booleanText; - return true; - } - if (value != null && field.StaticLookupValues?.TryGetValue(value, out var displayText) == true) { text = displayText; return true; } - text = value ?? string.Empty; + if (DynamicReportImageHelper.TryFormatReportValue(field.Field, rawValue, field.EditorType, out var formattedValue)) + { + text = formattedValue; + return true; + } + + text = Convert.ToString(rawValue, CultureInfo.CurrentCulture) ?? string.Empty; return true; } @@ -439,12 +449,68 @@ public class DynamicFormReport : XtraReport { var groupHeight = groups .Sum(group => (string.IsNullOrWhiteSpace(group.Caption) ? 0 : 26) + - group.Fields.Sum(field => GetFieldRowHeight(field) + 3) + 8); + CalculateGroupFieldsHeight(group) + 8); var subFormHeight = subFormCount * 32; return Math.Max(groupHeight + subFormHeight, RowHeight); } + private static float CalculateGroupFieldsHeight(FormGroup group) + { + var columnCount = GetSafeColumnCount(group.ColumnCount); + return CreateLayoutRows(group.Fields, columnCount) + .Sum(row => row.Max(x => GetFieldRowHeight(x.Field)) + 3); + } + + private static float GetCellWidth(float pageWidth, int columnCount) + { + return (pageWidth - ColumnGap * (columnCount - 1)) / columnCount; + } + + private static int GetSafeColumnCount(int columnCount) + { + return Math.Max(columnCount, 1); + } + + private static int GetSafeColumnSpan(int columnSpan, int columnCount) + { + return Math.Clamp(columnSpan <= 0 ? 1 : columnSpan, 1, columnCount); + } + + private static IEnumerable> CreateLayoutRows( + IReadOnlyList fields, + int columnCount) + { + var row = new List(); + var currentColumn = 0; + + foreach (var field in fields) + { + var columnSpan = GetSafeColumnSpan(field.ColumnSpan, columnCount); + if (currentColumn > 0 && currentColumn + columnSpan > columnCount) + { + yield return row; + row = []; + currentColumn = 0; + } + + row.Add(new FormLayoutItem(field, currentColumn, columnSpan)); + currentColumn += columnSpan; + + if (currentColumn >= columnCount) + { + yield return row; + row = []; + currentColumn = 0; + } + } + + if (row.Count > 0) + { + yield return row; + } + } + private static float GetFieldRowHeight(FormReportField field) { if (field.IsImage) @@ -475,12 +541,12 @@ public class DynamicFormReport : XtraReport var orderedFields = formItems.Count > 0 ? formItems .Where(x => fieldsByName.ContainsKey(x.DataField)) - .Select(x => new FieldWithEditor(fieldsByName[x.DataField], x.EditorType2)) + .Select(x => new FieldWithEditor(fieldsByName[x.DataField], x.EditorType2, x.ColSpan)) .ToList() : fields .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") .Where(x => x.IsActive != false && x.Visible == true) - .Select(x => new FieldWithEditor(x, null)) + .Select(x => new FieldWithEditor(x, null, 1)) .ToList(); var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields); @@ -489,7 +555,7 @@ public class DynamicFormReport : XtraReport .Select(x => { var selectField = selectFieldsByName[x.Field.FieldName]; - return CreateFormReportField(x.Field, selectField, x.EditorType, defaultValueHelper); + return CreateFormReportField(x.Field, selectField, x.EditorType, x.ColumnSpan, defaultValueHelper); }) .Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined") .ToList(); @@ -501,7 +567,7 @@ public class DynamicFormReport : XtraReport var groups = GetEditingFormGroups(listForm).ToList(); if (groups.Count == 0) { - yield return new FormGroup(null, fields); + yield return new FormGroup(null, 1, fields); yield break; } @@ -521,7 +587,7 @@ public class DynamicFormReport : XtraReport if (groupFields.Count > 0) { - yield return new FormGroup(group.Caption, groupFields); + yield return new FormGroup(group.Caption, group.ColCount, groupFields); } } @@ -530,7 +596,7 @@ public class DynamicFormReport : XtraReport .ToList(); if (remainingFields.Count > 0) { - yield return new FormGroup(null, remainingFields); + yield return new FormGroup(null, 1, remainingFields); } } @@ -609,6 +675,7 @@ public class DynamicFormReport : XtraReport ListFormField field, SelectField selectField, string editorType, + int columnSpan, DefaultValueHelper defaultValueHelper) { var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField); @@ -616,12 +683,12 @@ public class DynamicFormReport : XtraReport if (lookup == null || selectField?.JoinOptions != null) { - return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); + return new FormReportField(field, bindingName, editorType, columnSpan, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); } if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup)) { - return new FormReportField(field, DynamicReportSqlBuilder.GetLookupBindingName(field), editorType, lookup, null, false); + return new FormReportField(field, DynamicReportSqlBuilder.GetLookupBindingName(field), editorType, columnSpan, lookup, null, false); } if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData) @@ -631,12 +698,13 @@ public class DynamicFormReport : XtraReport field, staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName, editorType, + columnSpan, lookup, staticLookupValues, false); } - return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); + return new FormReportField(field, bindingName, editorType, columnSpan, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); } private static string CreateKeyFilter(string keyFieldName, string id) @@ -660,47 +728,19 @@ public class DynamicFormReport : XtraReport return null; } - private static bool IsBooleanEditor(FormReportField field) - { - return field.EditorType is PlatformConsts.EditorTypes.dxCheckBox or PlatformConsts.EditorTypes.dxSwitch || - field.Field.SourceDbType == DbType.Boolean; - } - private static bool IsHtmlEditor(FormReportField field) { return field.EditorType == PlatformConsts.EditorTypes.dxHtmlEditor; } - private static bool TryFormatBoolean(string value, out string text) - { - if (bool.TryParse(value, out var boolean)) - { - text = boolean ? "Evet" : "Hayır"; - return true; - } - - if (value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)) - { - text = "Evet"; - return true; - } - - if (value == "0" || string.Equals(value, "false", StringComparison.OrdinalIgnoreCase)) - { - text = "Hayır"; - return true; - } - - text = null; - return false; - } - - private sealed record FieldWithEditor(ListFormField Field, string EditorType); - private sealed record FormGroup(string Caption, IReadOnlyList Fields); + private sealed record FieldWithEditor(ListFormField Field, string EditorType, int ColumnSpan); + private sealed record FormGroup(string Caption, int ColumnCount, IReadOnlyList Fields); + private sealed record FormLayoutItem(FormReportField Field, int ColumnIndex, int ColumnSpan); private sealed record FormReportField( ListFormField Field, string BindingName, string EditorType, + int ColumnSpan, LookupDto Lookup, Dictionary StaticLookupValues, bool IsImage) : DynamicReportSqlBuilder.IReportLookupColumn; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 7d8b130..d787038 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -125,6 +125,7 @@ public class DynamicGridReport : XtraReport field, selectField, defaultValueHelper, + editorType, DynamicReportImageHelper.IsImageEditor(editorType), editorType == PlatformConsts.EditorTypes.dxHtmlEditor); }) @@ -366,7 +367,10 @@ public class DynamicGridReport : XtraReport Multiline = false, CanGrow = false }; - cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", $"[{column.BindingName}]")); + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Text", + DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", field, column.EditorType))); return cell; } @@ -412,7 +416,10 @@ public class DynamicGridReport : XtraReport return cell; } - cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", $"sumSum([{column.BindingName}])")); + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Text", + DynamicReportImageHelper.CreateFormatValueExpression($"sumSum([{column.BindingName}])", field, column.EditorType))); cell.Summary = new XRSummary { Running = SummaryRunning.Report, @@ -509,6 +516,7 @@ public class DynamicGridReport : XtraReport ListFormField field, SelectField selectField, DefaultValueHelper defaultValueHelper, + string editorType, bool isImage, bool isHtml) { @@ -517,12 +525,12 @@ public class DynamicGridReport : XtraReport if (lookup == null || selectField?.JoinOptions != null) { - return new ReportColumn(field, bindingName, null, null, isImage, isHtml); + 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); + return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isImage, isHtml, editorType); } if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData) @@ -534,10 +542,11 @@ public class DynamicGridReport : XtraReport lookup, staticLookupValues, isImage, - isHtml); + isHtml, + editorType); } - return new ReportColumn(field, bindingName, null, null, isImage, isHtml); + return new ReportColumn(field, bindingName, null, null, isImage, isHtml, editorType); } private sealed record ReportColumn( @@ -547,6 +556,7 @@ public class DynamicGridReport : XtraReport Dictionary StaticLookupValues, bool IsImage, bool IsHtml, + string EditorType, int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn; private static SummaryFunc? GetSummaryType(string totalSummaryJson) diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs index da5f364..38ba5ba 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs @@ -35,7 +35,8 @@ public partial class DynamicReport : XtraReport // Language verilerini yükle LoadLanguageParameters(); - + + pageInfo1.TextFormatString = "{0:d}"; xrRichText1.BeforePrint += XrRichText1_BeforePrint; } @@ -58,11 +59,11 @@ public partial class DynamicReport : XtraReport SetParameter("name", language.DisplayName); } - SetParameter("date", DateTime.Now.ToString("dd.MM.yyyy")); + SetParameter("date", DynamicReportImageHelper.FormatReportDate(DateTime.Now)); } catch { - SetParameter("date", DateTime.Now.ToString("dd.MM.yyyy")); + SetParameter("date", DynamicReportImageHelper.FormatReportDate(DateTime.Now)); } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs index 7e4191c..78bc3cb 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs @@ -32,6 +32,7 @@ internal static class DynamicReportImageHelper private const int MaxFormImages = 8; private const string ImageUrlFunctionName = "SozReportImageUrl"; 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*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); @@ -47,6 +48,7 @@ internal static class DynamicReportImageHelper { RegisterImageFunction(new ReportImageUrlFunction()); RegisterImageFunction(new ReportImageSourceFunction()); + RegisterImageFunction(new ReportFormatValueFunction()); } public static Dictionary CreateSelectFieldMap(IEnumerable selectFields) @@ -449,6 +451,261 @@ internal static class DynamicReportImageHelper !string.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture)); } + public static IDisposable ApplyReportCulture(IDictionary parameters) + { + var previousCulture = CultureInfo.CurrentCulture; + var previousUiCulture = CultureInfo.CurrentUICulture; + + if (TryGetCultureName(parameters, out var cultureName)) + { + try + { + var culture = CultureInfo.GetCultureInfo(cultureName); + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + System.Threading.Thread.CurrentThread.CurrentCulture = culture; + System.Threading.Thread.CurrentThread.CurrentUICulture = culture; + } + catch (CultureNotFoundException) + { + } + } + + return new RestoreCultureScope(previousCulture, previousUiCulture); + } + + public static bool TryFormatReportValue( + ListFormField field, + object value, + string editorType, + out string text) + { + return TryFormatReportValue(value, field.SourceDbType, field.Format, editorType, out text); + } + + public static string CreateFormatValueExpression( + string valueExpression, + ListFormField field, + string editorType) + { + return $"{FormatValueFunctionName}({valueExpression}, '{field.SourceDbType}', '{EscapeExpressionString(field.Format)}', '{EscapeExpressionString(editorType)}')"; + } + + public static string FormatReportDate(DateTime value) + { + var culture = CultureInfo.CurrentCulture; + return value.ToString(culture.DateTimeFormat.ShortDatePattern, culture); + } + + private static bool TryFormatReportValue( + object value, + DbType dbType, + string format, + string editorType, + out string text) + { + text = null; + if (value == null || value == DBNull.Value) + { + text = string.Empty; + return true; + } + + var invariantValue = Convert.ToString(value, CultureInfo.InvariantCulture); + if (IsBooleanField(editorType, dbType) && TryFormatBoolean(invariantValue, out text)) + { + return true; + } + + return TryFormatDateValue(dbType, format, value, out text) || + TryFormatNumericValue(dbType, format, value, out text); + } + + private static bool TryGetCultureName(IDictionary parameters, out string cultureName) + { + foreach (var name in new[] { "cultureName", "culture", "ui-culture" }) + { + if (parameters.TryGetValue(name, out cultureName) && + !string.IsNullOrWhiteSpace(cultureName)) + { + cultureName = Uri.UnescapeDataString(cultureName); + return true; + } + } + + cultureName = null; + return false; + } + + private static bool TryFormatDateValue(DbType dbType, string format, object value, out string text) + { + text = null; + if (!IsDateField(dbType) || !TryConvertToDateTime(value, out var date)) + { + return false; + } + + var culture = CultureInfo.CurrentCulture; + var customFormat = GetCustomFormat(format); + if (!string.IsNullOrWhiteSpace(customFormat)) + { + try + { + text = date.ToString(customFormat, culture); + return true; + } + catch (FormatException) + { + } + } + + text = dbType == DbType.Date || date.TimeOfDay == TimeSpan.Zero + ? date.ToString(culture.DateTimeFormat.ShortDatePattern, culture) + : date.ToString("g", culture); + return true; + } + + private static bool TryFormatNumericValue(DbType dbType, string format, object value, out string text) + { + text = null; + if (!IsNumeric(dbType) || !TryConvertToDecimal(value, out var number)) + { + return false; + } + + var culture = CultureInfo.CurrentCulture; + var customFormat = GetCustomFormat(format); + if (!string.IsNullOrWhiteSpace(customFormat)) + { + try + { + text = number.ToString(customFormat, culture); + return true; + } + catch (FormatException) + { + } + } + + text = number.ToString("G29", culture); + return true; + } + + private static bool TryFormatBoolean(string value, out string text) + { + if (bool.TryParse(value, out var boolean)) + { + text = FormatBoolean(boolean); + return true; + } + + if (value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)) + { + text = FormatBoolean(true); + return true; + } + + if (value == "0" || string.Equals(value, "false", StringComparison.OrdinalIgnoreCase)) + { + text = FormatBoolean(false); + return true; + } + + text = null; + return false; + } + + private static string FormatBoolean(bool value) + { + return CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("tr", StringComparison.OrdinalIgnoreCase) + ? value ? "Evet" : "Hayır" + : value ? "Yes" : "No"; + } + + private static bool IsBooleanField(string editorType, DbType dbType) + { + return editorType is PlatformConsts.EditorTypes.dxCheckBox or PlatformConsts.EditorTypes.dxSwitch || + dbType == DbType.Boolean; + } + + private static string GetCustomFormat(string format) + { + return string.IsNullOrWhiteSpace(format) ? null : format; + } + + private static bool IsDateField(DbType dbType) + { + return dbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset; + } + + private static bool TryConvertToDateTime(object value, out DateTime date) + { + switch (value) + { + case DateTime dateTime: + date = dateTime; + return true; + case DateTimeOffset dateTimeOffset: + date = dateTimeOffset.LocalDateTime; + return true; + } + + var text = Convert.ToString(value, CultureInfo.InvariantCulture); + if (DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal, out date) || + DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date)) + { + return true; + } + + date = default; + return false; + } + + private static bool TryConvertToDecimal(object value, out decimal number) + { + if (value is IConvertible) + { + try + { + number = Convert.ToDecimal(value, CultureInfo.InvariantCulture); + return true; + } + catch (FormatException) + { + } + catch (InvalidCastException) + { + } + catch (OverflowException) + { + } + } + + var text = Convert.ToString(value, CultureInfo.InvariantCulture); + return decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out number) || + decimal.TryParse(text, NumberStyles.Any, CultureInfo.CurrentCulture, out number); + } + + private sealed class RestoreCultureScope : IDisposable + { + private readonly CultureInfo _culture; + private readonly CultureInfo _uiCulture; + + public RestoreCultureScope(CultureInfo culture, CultureInfo uiCulture) + { + _culture = culture; + _uiCulture = uiCulture; + } + + public void Dispose() + { + CultureInfo.CurrentCulture = _culture; + CultureInfo.CurrentUICulture = _uiCulture; + System.Threading.Thread.CurrentThread.CurrentCulture = _culture; + System.Threading.Thread.CurrentThread.CurrentUICulture = _uiCulture; + } + } + private static bool HasImageValue(object value) { return HasValue(value) && NormalizeImageValue(value).Any(IsPotentialImageSource); @@ -1043,4 +1300,47 @@ internal static class DynamicReportImageHelper return GetImageSourceObjectAt(rawValue, baseUrl, index); } } + + private sealed class ReportFormatValueFunction : ICustomFunctionOperatorBrowsable + { + public string Name => FormatValueFunctionName; + public int MinOperandCount => 4; + public int MaxOperandCount => 4; + public FunctionCategory Category => FunctionCategory.Text; + public string Description => "Formats a report value using the active culture."; + + public object Evaluate(params object[] operands) + { + var value = operands.ElementAtOrDefault(0); + var dbType = ToDbType(Convert.ToString(operands.ElementAtOrDefault(1), CultureInfo.InvariantCulture)); + var format = Convert.ToString(operands.ElementAtOrDefault(2), CultureInfo.InvariantCulture); + var editorType = Convert.ToString(operands.ElementAtOrDefault(3), CultureInfo.InvariantCulture); + + return TryFormatReportValue(value, dbType, format, editorType, out var text) + ? text + : Convert.ToString(value, CultureInfo.CurrentCulture) ?? string.Empty; + } + + public Type ResultType(params Type[] operands) + { + return typeof(string); + } + + public bool IsValidOperandCount(int count) + { + return count == 4; + } + + public bool IsValidOperandType(int operandIndex, int operandCount, Type type) + { + return true; + } + + private static DbType ToDbType(string value) + { + return Enum.TryParse(value, true, out var dbType) + ? dbType + : DbType.String; + } + } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index aabb060..5b8f913 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -157,6 +157,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens try { var (reportName, parameters) = ParseReportUrl(url); + using var _ = Sozsoft.Reports.PredefinedReports.DynamicReportImageHelper.ApplyReportCulture(parameters); if (IsDynamicGridReport(reportName)) { @@ -238,6 +239,11 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens var filters = new List(); foreach (var param in parameters) { + if (IsReportInfrastructureParameter(param.Key)) + { + continue; + } + var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1); filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'"); } @@ -245,6 +251,13 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens query.FilterString = string.Join(" AND ", filters); } + private static bool IsReportInfrastructureParameter(string name) + { + return string.Equals(name, "cultureName", StringComparison.OrdinalIgnoreCase) || + string.Equals(name, "culture", StringComparison.OrdinalIgnoreCase) || + string.Equals(name, "ui-culture", StringComparison.OrdinalIgnoreCase); + } + private static (string ReportName, Dictionary Parameters) ParseReportUrl(string url) { var decodedUrl = DecodeRepeatedly(url); diff --git a/ui/src/views/report/DevexpressReportViewer.tsx b/ui/src/views/report/DevexpressReportViewer.tsx index aed8108..0768500 100644 --- a/ui/src/views/report/DevexpressReportViewer.tsx +++ b/ui/src/views/report/DevexpressReportViewer.tsx @@ -96,9 +96,14 @@ const DevexpressReportViewer: React.FC = () => { const location = useLocation() const token = useStoreState((state) => state.auth.session.token) const themeMode = useStoreState((state) => state.theme.mode) + const cultureName = useStoreState((state) => state.locale.currentLang) + const configCultureName = useStoreState( + (state) => state.abpConfig.config?.localization.currentCulture.cultureName, + ) const [isRequestAuthReady, setIsRequestAuthReady] = useState(false) const previewModelRef = useRef(null) const isDarkMode = themeMode === MODE_DARK + const reportCultureName = cultureName || configCultureName const getReportPreview = useCallback((eventOrModel?: any) => { const eventArgs = eventOrModel?.args @@ -148,19 +153,33 @@ const DevexpressReportViewer: React.FC = () => { ajaxSetup.ajaxSettings.headers = { ...ajaxHeaders, Authorization: authorization, + ...(reportCultureName ? { 'Accept-Language': reportCultureName } : {}), } setIsRequestAuthReady(true) return } - const { Authorization: _ajaxAuthorization, ...nextAjaxHeaders } = ajaxHeaders + const { + Authorization: _ajaxAuthorization, + 'Accept-Language': _ajaxAcceptLanguage, + ...nextAjaxHeaders + } = ajaxHeaders + if (reportCultureName) { + nextAjaxHeaders['Accept-Language'] = reportCultureName + } ajaxSetup.ajaxSettings.headers = nextAjaxHeaders setIsRequestAuthReady(true) - }, [token]) + }, [token, reportCultureName]) const reportUrlWithParams = useMemo(() => { - return createReportUrl({ report, id, listFormCode }, location.search) - }, [report, id, listFormCode, location.search]) + const searchParams = new URLSearchParams(location.search) + if (reportCultureName) { + searchParams.set('cultureName', reportCultureName) + } + + const search = searchParams.toString() + return createReportUrl({ report, id, listFormCode }, search ? `?${search}` : '') + }, [report, id, listFormCode, location.search, reportCultureName]) if (!reportUrlWithParams) return null