From 203160fce0fe5f8aa28d06b8fb6a8ab0e8cb57fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Sun, 22 Mar 2026 03:56:24 +0300 Subject: [PATCH] =?UTF-8?q?Note=20ve=20AuditLog=20k=C4=B1sm=C4=B1=20d?= =?UTF-8?q?=C3=BCzenlendi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuditLogs/AuditLogDto.cs | 2 + .../ListForms/ListFormSelectAppService.cs | 2 +- .../Seeds/LanguagesData.json | 102 +++ .../Seeds/ListFormSeeder_Administration.cs | 4 +- .../Seeds/ListFormSeeder_DefaultJsons.cs | 4 +- .../Seeds/ListFormSeeder_Saas.cs | 28 +- .../ListForms/ListFormManager.cs | 2 +- .../Queries/DefaultValueManager.cs | 72 +- ui/src/proxy/auditLog/audit-log.ts | 2 + ui/src/services/identity.service.ts | 9 +- ui/src/views/form/notes/NoteList.tsx | 634 +++++++++++++++--- ui/src/views/form/notes/NoteModal.tsx | 36 +- ui/src/views/form/notes/NotePanel.tsx | 49 +- 13 files changed, 756 insertions(+), 190 deletions(-) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogDto.cs index 77b5f0d..9da1912 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogDto.cs @@ -15,6 +15,8 @@ public class AuditLogDto : EntityDto public string ApplicationName { get; set; } public Guid? UserId { get; protected set; } public string UserName { get; protected set; } + public Guid? TenantId { get; protected set; } + public string TenantName { get; protected set; } public DateTime ExecutionTime { get; protected set; } public int ExecutionDuration { get; protected set; } public string ClientIpAddress { get; protected set; } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs index b2c0899..dd862f3 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs @@ -306,7 +306,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe }; var queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value); - var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, Enums.OperationEnum.Select, queryParameters: queryParameters); + var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, Enums.OperationEnum.Select, queryParameters: queryParameters); // Performans: Dictionary ile hızlı lookup var columnFormatsDict = result.ColumnFormats.ToDictionary(c => c.FieldName, c => c); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index e19164c..d715836 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -1008,12 +1008,30 @@ "en": "Cancel", "tr": "İptal" }, + { + "resourceName": "Platform", + "key": "Download", + "en": "Download", + "tr": "İndir" + }, + { + "resourceName": "Platform", + "key": "Insert", + "en": "Insert", + "tr": "Ekle" + }, { "resourceName": "Platform", "key": "Delete", "en": "Delete", "tr": "Sil" }, + { + "resourceName": "Platform", + "key": "Operation", + "en": "Operation", + "tr": "İşlem" + }, { "resourceName": "Platform", "key": "App.Platform.HangfireLogin", @@ -3270,6 +3288,90 @@ "en": "Refresh", "tr": "Tazele" }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.AddNote", + "en": "Add Note", + "tr": "Not Ekle" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NotesPanel.DownloadFailed", + "en": "Download Failed", + "tr": "İndirme Başarısız" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Upload.MaxSize2Mb", + "en": "Maximum file size is 2MB", + "tr": "Maksimum dosya boyutu 2MB'dir" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NotesPanel.ClosePanel", + "en": "Close Panel", + "tr": "Paneli Kapat" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Subject", + "en": "Enter a short title...", + "tr": "Kısa bir başlık girin..." + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Content", + "en": "Enter note content...", + "tr": "Notunuzu buraya yazın..." + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NotesPanel.OpenPanel", + "en": "Open Panel", + "tr": "Paneli Aç" + }, + { + "resourceName": "Platform", + "key": "App.AuditLogs.FetchFailed", + "en": "Fetch Failed", + "tr": "Veri Getirme Başarısız" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.Notes", + "en": "Notes", + "tr": "Notlar" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.Notes.Empty", + "en": "No notes yet", + "tr": "Henüz hiçbir not bulunmuyor" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Type.Note", + "en": "Note", + "tr": "Not" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Type.Message", + "en": "Message", + "tr": "Mesaj" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.NoteModal.Type.Activity", + "en": "Activity", + "tr": "Aktivite" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.AuditLogs.Empty", + "en": "No audit logs yet", + "tr": "Henüz hiçbir audit logu bulunmuyor" + }, { "resourceName": "Platform", "key": "ListForms.ListForm.DeleteFilter", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 58b8d66..cef4281 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -1722,7 +1722,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Sector)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), PagerOptionJson = DefaultPagerOptionJson, - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List { @@ -1815,7 +1815,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.WorkHour)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs index f07f270..6a148c2 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs @@ -15,12 +15,12 @@ public static class ListFormSeeder_DefaultJsons return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id"; } - public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid) => JsonSerializer.Serialize(new FieldsDefaultValue[] + public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "CreationTime", FieldDbType = DbType.DateTime, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, new() { FieldName = "IsDeleted", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }, - new() { FieldName = "Id", FieldDbType = dbType, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey } + new() { FieldName = "Id", FieldDbType = dbType, Value = newId, CustomValueType = FieldCustomValueTypeEnum.CustomKey } }); public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid) => JsonSerializer.Serialize(new FieldsDefaultValue[] diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs index fea3d75..21398d5 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs @@ -1478,6 +1478,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Currency)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List @@ -1485,14 +1486,13 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency new() { Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group", Items = [ + new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 2, DataField = "Symbol", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, - new EditingFormItemDto { Order = 3, DataField = "Name", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, - new EditingFormItemDto { Order = 4, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox }, - new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox } + new EditingFormItemDto { Order = 3, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox }, + new EditingFormItemDto { Order = 4, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox } ] } }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), }); #region Currency Fields @@ -1623,6 +1623,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CountryGroup)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List @@ -1634,7 +1635,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ] } }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), }); #region CountryGroup Fields @@ -1713,6 +1713,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Country)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List @@ -1730,7 +1731,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ] } }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "Currency", FieldDbType = DbType.String, Value = "TRY", CustomValueType = FieldCustomValueTypeEnum.Value } }) @@ -2210,6 +2210,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.UomCategory)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, false, false, true), EditingFormJson = JsonSerializer.Serialize(new List @@ -2221,7 +2222,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ] } }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), SubFormsJson = JsonSerializer.Serialize(new List() { new { TabType = ListFormTabTypeEnum.List, @@ -2316,6 +2316,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Uom)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -2328,7 +2329,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2=EditorTypes.dxCheckBox }, ]} }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value } }), @@ -2520,7 +2520,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillType)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), PagerOptionJson = DefaultPagerOptionJson, - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false, true), EditingFormJson = JsonSerializer.Serialize(new List { @@ -2638,7 +2638,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillLevel)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), PagerOptionJson = DefaultPagerOptionJson, - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items= @@ -2784,7 +2784,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Skill)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), PagerOptionJson = DefaultPagerOptionJson, - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), EditingOptionJson = DefaultEditingOptionJson(AppCodes.Definitions.SkillLevel, 600, 300, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ @@ -5115,7 +5115,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency }), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.MenuGroup)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), }); #region MenuGroup Fields @@ -5217,7 +5217,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency new EditingFormItemDto { Order = 12, DataField = "ShortName", ColSpan = 1, EditorType2=EditorTypes.dxTextBox }, ]} }), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Code"), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "IsDisabled", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value } }) @@ -5992,7 +5992,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency }), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.PaymentMethod)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), - InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value } }), diff --git a/api/src/Sozsoft.Platform.Domain/ListForms/ListFormManager.cs b/api/src/Sozsoft.Platform.Domain/ListForms/ListFormManager.cs index c4aedef..1efcae4 100644 --- a/api/src/Sozsoft.Platform.Domain/ListForms/ListFormManager.cs +++ b/api/src/Sozsoft.Platform.Domain/ListForms/ListFormManager.cs @@ -106,7 +106,7 @@ public class ListFormManager : PlatformDomainService, IListFormManager } // ListFormField icerisinde olmayan (select sorgusu ile alinmayan) deger almasi gereken zorunlu alanlar - var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, op, keys, queryParameters); + var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, listFormFields, op, keys, queryParameters, inputParams); if (PlatformConsts.IsMultiTenant && listForm.IsTenant) { diff --git a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs index a4c66f1..524509f 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs @@ -20,9 +20,11 @@ public interface IDefaultValueManager { Task> GenerateDefaultValuesAsync( ListForm listForm, + List listFormFields, OperationEnum op, object[] keys = null, - Dictionary queryParameters = null); + Dictionary queryParameters = null, + dynamic inputParams = null); } public class DefaultValueManager : PlatformDomainService, IDefaultValueManager @@ -39,9 +41,11 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager public async Task> GenerateDefaultValuesAsync( ListForm listForm, + List listFormFields, OperationEnum op, object[] keys = null, - Dictionary queryParameters = null) + Dictionary queryParameters = null, + dynamic inputParams = null) { var fields = new Dictionary(); @@ -60,69 +64,87 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager return fields; } - foreach (var field in JsonSerializer.Deserialize>(defaultFieldsJson)) + foreach (var defaultField in JsonSerializer.Deserialize>(defaultFieldsJson)) { object value = null; - switch (field.CustomValueType) + switch (defaultField.CustomValueType) { case FieldCustomValueTypeEnum.CustomKey: - if (field.Value == PlatformConsts.DefaultValues.UserId) + if (defaultField.Value == PlatformConsts.DefaultValues.UserId) value = CurrentUser.Id; - else if (field.Value == PlatformConsts.DefaultValues.UserName) + else if (defaultField.Value == PlatformConsts.DefaultValues.UserName) value = CurrentUser.Name; - else if (field.Value == PlatformConsts.DefaultValues.Roles) + else if (defaultField.Value == PlatformConsts.DefaultValues.Roles) value = CurrentUser.Roles; //.JoinAsString("','"); - else if (field.Value == PlatformConsts.DefaultValues.Date) + else if (defaultField.Value == PlatformConsts.DefaultValues.Date) value = Clock.Now.Date; - else if (field.Value == PlatformConsts.DefaultValues.Now) + else if (defaultField.Value == PlatformConsts.DefaultValues.Now) value = Clock.Now; - else if (field.Value == PlatformConsts.DefaultValues.Day) + else if (defaultField.Value == PlatformConsts.DefaultValues.Day) value = Clock.Now.Day; - else if (field.Value == PlatformConsts.DefaultValues.Month) + else if (defaultField.Value == PlatformConsts.DefaultValues.Month) value = Clock.Now.Month; - else if (field.Value == PlatformConsts.DefaultValues.Year) + else if (defaultField.Value == PlatformConsts.DefaultValues.Year) value = Clock.Now.Year; - else if (field.Value == PlatformConsts.DefaultValues.Id) + else if (defaultField.Value == PlatformConsts.DefaultValues.Id) value = keys?.FirstOrDefault(); - else if (field.Value == PlatformConsts.DefaultValues.NewId) + else if (defaultField.Value == PlatformConsts.DefaultValues.NewId) value = Guid.NewGuid(); - else if (field.Value == PlatformConsts.DefaultValues.Selected_Ids) + else if (defaultField.Value == PlatformConsts.DefaultValues.Selected_Ids) value = keys; - else if (field.Value == PlatformConsts.DefaultValues.TenantId) + else if (defaultField.Value == PlatformConsts.DefaultValues.TenantId) value = CurrentTenant.Id; else - value = field.Value; + { + if ((object)inputParams != null) + { + foreach (var item in JsonSerializer.Deserialize>(inputParams)) + { + var field = listFormFields.FirstOrDefault(c => c.FieldName == item.Key); + if (field == null + || (op == OperationEnum.Insert && !field.CanCreate) + || (op == OperationEnum.Update && !field.CanUpdate) + ) + { + continue; + } + + value = QueryHelper.GetFormattedValue(field.SourceDbType, item.Value); + } + } + } + //value = field.Value; //TODO: artirilabilir break; case FieldCustomValueTypeEnum.QueryParams: if (queryParameters != null) { - value = queryParameters.GetValueOrDefault(field.Value); + value = queryParameters.GetValueOrDefault(defaultField.Value); } break; case FieldCustomValueTypeEnum.DbQuery: - if (!string.IsNullOrWhiteSpace(field.SqlQuery)) + if (!string.IsNullOrWhiteSpace(defaultField.SqlQuery)) { var dynamicDataManager = LazyServiceProvider.LazyGetRequiredService(); var (dynamicDataRepository, connectionString, _) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); - value = await dynamicDataRepository.ExecuteScalarAsync(field.SqlQuery, connectionString); + value = await dynamicDataRepository.ExecuteScalarAsync(defaultField.SqlQuery, connectionString); } break; case FieldCustomValueTypeEnum.Value: default: - value = field.Value; + value = defaultField.Value; break; } if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) { - var formattedValue = QueryHelper.GetFormattedValue(field.FieldDbType, value); - if (fields.ContainsKey(field.FieldName)) + var formattedValue = QueryHelper.GetFormattedValue(defaultField.FieldDbType, value); + if (fields.ContainsKey(defaultField.FieldName)) { - fields[field.FieldName] = formattedValue; + fields[defaultField.FieldName] = formattedValue; } else { - fields.Add(field.FieldName, formattedValue); + fields.Add(defaultField.FieldName, formattedValue); } } } diff --git a/ui/src/proxy/auditLog/audit-log.ts b/ui/src/proxy/auditLog/audit-log.ts index 0205509..9f430b3 100644 --- a/ui/src/proxy/auditLog/audit-log.ts +++ b/ui/src/proxy/auditLog/audit-log.ts @@ -26,6 +26,8 @@ export interface AuditLogDto { applicationName: string userId?: string userName?: string + tenantId?: string + tenantName?: string executionTime: string executionDuration: number clientIpAddress?: string diff --git a/ui/src/services/identity.service.ts b/ui/src/services/identity.service.ts index 9f4236b..d626d45 100644 --- a/ui/src/services/identity.service.ts +++ b/ui/src/services/identity.service.ts @@ -7,7 +7,7 @@ import { UserClaimModel, UserInfoViewModel, } from '@/proxy/admin/models' -import { ListResultDto } from '../proxy' +import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '../proxy' import { AuditLogDto } from '../proxy/auditLog/audit-log' import apiService from './api.service' @@ -74,6 +74,13 @@ export const getAuditLogs = (id: string) => url: `/api/app/audit-log/${id}`, }) +export const getAuditLogList = (input: PagedAndSortedResultRequestDto) => + apiService.fetchData, PagedAndSortedResultRequestDto>({ + method: 'GET', + url: '/api/app/audit-log', + params: input, + }) + export const postClaimUser = (input: UserClaimModel) => apiService.fetchData({ method: 'POST', diff --git a/ui/src/views/form/notes/NoteList.tsx b/ui/src/views/form/notes/NoteList.tsx index e49e28c..b91cd8c 100644 --- a/ui/src/views/form/notes/NoteList.tsx +++ b/ui/src/views/form/notes/NoteList.tsx @@ -1,22 +1,62 @@ -import React from 'react' -import { FaStickyNote, FaEnvelope, FaTrash, FaDownload, FaClock, FaPaperclip } from 'react-icons/fa' -import { Avatar, Button } from '@/components/ui' +import React, { useEffect, useMemo, useState } from 'react' +import { + FaStickyNote, + FaEnvelope, + FaPlus, + FaTrash, + FaDownload, + FaClock, + FaPaperclip, + FaHistory, + FaSyncAlt, +} from 'react-icons/fa' +import { Avatar, Badge, Button, Spinner, Tabs } from '@/components/ui' +import TabContent from '@/components/ui/Tabs/TabContent' +import TabList from '@/components/ui/Tabs/TabList' +import TabNav from '@/components/ui/Tabs/TabNav' import { NoteDto } from '@/proxy/note/models' import { AVATAR_URL } from '@/constants/app.constant' import { useStoreState } from '@/store/store' +import apiService from '@/services/api.service' +import { PagedResultDto } from '@/proxy' +import { AuditLogActionDto, AuditLogDto } from '@/proxy/auditLog/audit-log' +import { useLocalization } from '@/utils/hooks/useLocalization' interface NoteListProps { notes: NoteDto[] + entityName: string + entityId: string + onAddNote?: () => void onDeleteNote?: (noteId: string) => void onDownloadFile?: (fileData: any) => void } export const NoteList: React.FC = ({ notes, + entityName, + entityId, + onAddNote, onDeleteNote, onDownloadFile, }) => { const user = useStoreState((state) => state.auth.user) + const { translate } = useLocalization() + + const [currentTab, setCurrentTab] = useState<'notes' | 'audit'>('notes') + const [auditLoading, setAuditLoading] = useState(false) + const [auditError, setAuditError] = useState(null) + const [auditItems, setAuditItems] = useState< + Array<{ + log: AuditLogDto + matchedActions: Array<{ + action: AuditLogActionDto + input: any + operation: string + rowLabel?: string + }> + }> + >([]) + const [auditLoadedKey, setAuditLoadedKey] = useState(null) const getNoteStyle = (type: string) => { switch (type) { @@ -38,103 +78,511 @@ export const NoteList: React.FC = ({ } } - if (notes.length === 0) - return ( -
- -

Henüz hiçbir not bulunmuyor

-
- ) + const entityIdNormalized = useMemo( + () => + String(entityId ?? '') + .trim() + .toLowerCase(), + [entityId], + ) + + const listFormCodeNormalized = useMemo( + () => + String(entityName ?? '') + .trim() + .toLowerCase(), + [entityName], + ) + + const normalize = (value: unknown) => + String(value ?? '') + .trim() + .toLowerCase() + + const tryParseJson = (text: string) => { + if (!text) return null + try { + return JSON.parse(text) + } catch { + // sometimes parameters are a JSON string containing JSON + try { + const unwrapped = JSON.parse(`"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`) + return JSON.parse(unwrapped) + } catch { + // last resort: try to parse substring that looks like JSON + const first = text.indexOf('{') + const last = text.lastIndexOf('}') + if (first >= 0 && last > first) { + const slice = text.slice(first, last + 1) + try { + return JSON.parse(slice) + } catch { + return null + } + } + return null + } + } + } + + const getListFormInputFromAction = (action: AuditLogActionDto) => { + const parametersText = String(action?.parameters ?? '').trim() + if (!parametersText) return null + const parsed = tryParseJson(parametersText) + const input = parsed?.input + if (!input || typeof input !== 'object') return null + if (!('listFormCode' in input)) return null + return input as any + } + + const getKeysFromInput = (input: any): string[] => { + if (!input) return [] + if (!Array.isArray(input.keys)) return [] + return input.keys.filter((k: any) => k !== null && k !== undefined).map((k: any) => String(k)) + } + + const getOperationFromInput = ( + input: any, + action: AuditLogActionDto, + ): 'insert' | 'update' | 'delete' | 'unknown' => { + const keys = getKeysFromInput(input) + const hasKeys = keys.length > 0 + const data = input?.data + const hasData = !!data && typeof data === 'object' && Object.keys(data).length > 0 + + if (!hasKeys && hasData) return 'insert' + if (hasKeys && hasData) return 'update' + if (hasKeys && !hasData) return 'delete' + + const method = normalize(action?.methodName) + if (method.includes('create') || method.includes('insert') || method.includes('add')) + return 'insert' + if (method.includes('update') || method.includes('edit')) return 'update' + if (method.includes('delete') || method.includes('remove')) return 'delete' + return 'unknown' + } + + const findMatchingValueInData = ( + data: any, + targetNormalized: string, + depth = 0, + visited?: Set, + ): { value: any; path: string } | null => { + if (!targetNormalized) return null + if (data === null || data === undefined) return null + if (depth > 6) return null + + const type = typeof data + if (type === 'string' || type === 'number' || type === 'boolean') { + return normalize(data) === targetNormalized ? { value: data, path: '' } : null + } + + if (Array.isArray(data)) { + for (let i = 0; i < data.length; i++) { + const hit = findMatchingValueInData(data[i], targetNormalized, depth + 1, visited) + if (hit) return { value: hit.value, path: `[${i}]${hit.path ? '.' + hit.path : ''}` } + } + return null + } + + if (type === 'object') { + const visit = visited ?? new Set() + if (visit.has(data)) return null + visit.add(data) + + const entries = Object.entries(data as Record) + const preferredKeys = ['id', 'Id', 'name', 'Name', 'code', 'Code'] + const orderedEntries = [ + ...preferredKeys + .filter((k) => Object.prototype.hasOwnProperty.call(data, k)) + .map((k) => [k, (data as any)[k]] as const), + ...entries.filter(([k]) => !preferredKeys.includes(k)), + ] + + for (const [k, v] of orderedEntries) { + const hit = findMatchingValueInData(v, targetNormalized, depth + 1, visit) + if (hit) { + const childPath = hit.path ? '.' + hit.path : '' + return { value: hit.value, path: `${k}${childPath}` } + } + } + return null + } + + return null + } + + const getRowLabelIfMatches = (input: any): string | null => { + if (!entityIdNormalized) return null + const inputFormCode = normalize(input?.listFormCode) + if (!inputFormCode || inputFormCode !== listFormCodeNormalized) return null + + const keys = getKeysFromInput(input) + .map((k) => normalize(k)) + .filter(Boolean) + if (keys.length > 0) { + // update/delete should match by keys + if (keys.includes(entityIdNormalized)) { + return getKeysFromInput(input).join(', ') + } + + // Some entities may use a different PK than the visible row key; allow strict match via input.data too. + const data = input?.data + if (data && typeof data === 'object') { + const hit = findMatchingValueInData(data, entityIdNormalized) + if (hit) { + const nameValue = (data as any)?.Name ?? (data as any)?.name + return nameValue ? String(nameValue) : String(hit.value) + } + } + + return null + } + + // insert: keys is null, match by scanning input.data for entity id/name/code/etc. + const data = input?.data + if (data && typeof data === 'object') { + const hit = findMatchingValueInData(data, entityIdNormalized) + if (!hit) return null + + // Prefer showing data.Name if present; otherwise show matched value + const nameValue = (data as any)?.Name ?? (data as any)?.name + if (nameValue) return String(nameValue) + return String(hit.value) + } + + return null + } + + const buildMatchedActions = (log: AuditLogDto) => { + const actions = log.actions ?? [] + const matched: Array<{ + action: AuditLogActionDto + input: any + operation: string + rowLabel?: string + }> = [] + for (const action of actions) { + const input = getListFormInputFromAction(action) + if (!input) continue + const rowLabel = getRowLabelIfMatches(input) + if (rowLabel == null) continue + const operation = getOperationFromInput(input, action) + matched.push({ action, input, operation, rowLabel: String(rowLabel) }) + } + return matched + } + + const loadAuditLogs = async () => { + setAuditLoading(true) + setAuditError(null) + try { + const response = await apiService.fetchData>({ + method: 'GET', + url: '/api/app/audit-log', + params: { + skipCount: 0, + maxResultCount: 200, + sorting: 'ExecutionTime DESC', + }, + }) + + const items = response.data?.items ?? [] + const filtered = items + .map((log) => ({ log, matchedActions: buildMatchedActions(log) })) + .filter((x) => x.matchedActions.length > 0) + + setAuditItems(filtered) + setAuditLoadedKey(`${listFormCodeNormalized}|${entityIdNormalized}`) + } catch (e) { + console.error('Failed to fetch audit logs:', e) + setAuditError(translate('::App.AuditLogs.FetchFailed')) + setAuditItems([]) + setAuditLoadedKey(`${listFormCodeNormalized}|${entityIdNormalized}`) + } finally { + setAuditLoading(false) + } + } + + useEffect(() => { + if (currentTab !== 'audit') return + if (!listFormCodeNormalized && !entityIdNormalized) return + const key = `${listFormCodeNormalized}|${entityIdNormalized}` + if (auditLoadedKey === key) return + loadAuditLogs() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentTab, listFormCodeNormalized, entityIdNormalized]) + + const getStatusBadge = (statusCode?: number) => { + if (!statusCode) return + if (statusCode >= 200 && statusCode < 300) + return + if (statusCode >= 400 && statusCode < 500) + return + if (statusCode >= 500) return + return + } return (
-
- {notes.map((note, index) => { - const files = note.filesJson ? JSON.parse(note.filesJson) : [] - const creationDate = note.creationTime ? new Date(note.creationTime) : null - const { icon, border } = getNoteStyle(note.type) + setCurrentTab(val as 'notes' | 'audit')} + variant="pill" + > + + + {translate('::ListForms.ListForm.Notes')} + + + + {translate('::App.AuditLogs')} + + + - return ( -
+
+ +
-
- {/* Header */} -
-
-
- - {note.createUserName} -
- {creationDate && ( -
- {creationDate.toLocaleString()} -
- )} -
- - {/* Sil butonu */} - {user?.id === note.creatorId && ( - - )} -
- - {/* Body */} -
- {note.subject && ( -

{note.subject}

- )} - {note.content && ( -
- )} -
- - {/* Files */} - {files.length > 0 && ( -
- {files.map((file: any, index: number) => ( -
- - {file.FileName} - -
- ))} -
- )} -
+ {(notes?.length ?? 0) === 0 ? ( +
+ +

{translate('::ListForms.ListForm.Notes.Empty')}

- ) - })} -
+ ) : ( +
+ {notes.map((note, index) => { + const files = note.filesJson ? JSON.parse(note.filesJson) : [] + const creationDate = note.creationTime ? new Date(note.creationTime) : null + const { icon, border } = getNoteStyle(note.type) + + return ( +
+ {/* Timeline Düğmesi */} +
+ {icon} +
+ +
+ {/* Header */} +
+
+
+ + {note.createUserName} +
+ {creationDate && ( +
+ {creationDate.toLocaleString()} +
+ )} +
+ + {/* Sil butonu */} + {user?.id === note.creatorId && ( + + )} +
+ + {/* Body */} +
+ {note.subject && ( +

{note.subject}

+ )} + {note.content &&
} +
+ + {/* Files */} + {files.length > 0 && ( +
+ {files.map((file: any, index: number) => ( +
+ + {file.FileName} + +
+ ))} +
+ )} +
+
+ ) + })} +
+ )} + + + +
+ +
+ + {auditLoading ? ( +
+ +
+ ) : auditError ? ( +
{auditError}
+ ) : (auditItems?.length ?? 0) === 0 ? ( +
+ +

{translate('::ListForms.ListForm.AuditLogs.Empty')}

+
+ ) : ( +
+ {auditItems.map(({ log, matchedActions }) => { + const execTime = log.executionTime ? new Date(log.executionTime) : null + const changeCount = log.entityChangeCount ?? log.entityChanges?.length ?? 0 + + const primaryMatch = matchedActions?.[0] + const op = (primaryMatch?.operation ?? 'unknown') as string + const opLabel = + op === 'insert' + ? translate('::Insert') + : op === 'update' + ? translate('::Update') + : op === 'delete' + ? translate('::Delete') + : translate('::Operation') + const opClass = + op === 'insert' + ? 'bg-green-500' + : op === 'update' + ? 'bg-yellow-500' + : op === 'delete' + ? 'bg-red-500' + : 'bg-gray-500' + + const rowLabel = primaryMatch?.rowLabel ?? '' + + const propertyChanges = (log.entityChanges ?? []).flatMap( + (c) => c?.propertyChanges ?? [], + ) + const changeLines: string[] = [] + if (propertyChanges.length > 0) { + for (const pc of propertyChanges) { + changeLines.push( + `${pc.propertyName}: ${(pc.originalValue ?? '') || '∅'} → ${(pc.newValue ?? '') || '∅'}`, + ) + } + } else if ( + primaryMatch?.input?.data && + typeof primaryMatch.input.data === 'object' + ) { + const entries = Object.entries(primaryMatch.input.data as Record) + for (const [k, v] of entries) { + changeLines.push(`${k}: ${String(v)}`) + } + } + + return ( +
+
+ +
+ +
+
+
+
+ + + {log.userName} +
+ {execTime && ( +
+ {execTime.toLocaleString()} +
+ )} +
+ + {rowLabel ? : null} +
+
+ {(log.httpMethod || 'HTTP') + ' ' + (log.url || '')} +
+ {changeLines.length > 0 && ( +
+ {changeLines.slice(0, 8).map((line, idx) => ( +
+ {line} +
+ ))} + {changeLines.length > 8 && ( +
+ +{changeLines.length - 8} değişiklik daha +
+ )} +
+ )} +
+ +
+ {getStatusBadge(log.httpStatusCode)} + +
+
+
+
+ ) + })} +
+ )} +
+
) } diff --git a/ui/src/views/form/notes/NoteModal.tsx b/ui/src/views/form/notes/NoteModal.tsx index 716a550..2c3e8ce 100644 --- a/ui/src/views/form/notes/NoteModal.tsx +++ b/ui/src/views/form/notes/NoteModal.tsx @@ -13,11 +13,12 @@ import { headerOptions, } from '@/proxy/reports/data' import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor' +import { useLocalization } from '@/utils/hooks/useLocalization' const validationSchema = Yup.object({ - type: Yup.string().required('Not tipi zorunludur'), - subject: Yup.string().required('Konu zorunludur'), - content: Yup.string().required('İçerik zorunludur'), + type: Yup.string().required(), + subject: Yup.string().required(), + content: Yup.string().required(), }) interface NoteModalProps { @@ -37,11 +38,11 @@ export const NoteModal: React.FC = ({ }) => { const [uploading, setUploading] = useState(false) const [fileList, setFileList] = useState([]) - + const { translate } = useLocalization() const types = [ - { value: 'note', label: 'Not' }, - { value: 'message', label: 'Mesaj' }, - { value: 'activity', label: 'Aktivite' }, + { value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') }, + { value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') }, + { value: 'activity', label: translate('::ListForms.ListForm.NoteModal.Type.Activity') }, ] const handleSave = async (values: any) => { @@ -69,7 +70,7 @@ export const NoteModal: React.FC = ({ const beforeUpload = (files: FileList | null) => { if (!files) return true for (const f of Array.from(files)) { - if (f.size > 2 * 1024 * 1024) return 'En fazla 2MB dosya yükleyebilirsiniz' + if (f.size > 2 * 1024 * 1024) return translate('::ListForms.ListForm.NoteModal.Upload.MaxSize2Mb') } return true } @@ -87,7 +88,7 @@ export const NoteModal: React.FC = ({
- Not / Aktivite Ekle + {translate('::ListForms.ListForm.AddNote')}
@@ -104,11 +105,11 @@ export const NoteModal: React.FC = ({ invalid={!!(errors.type && touched.type)} errorMessage={errors.type} > -
+
{types.map((t) => (