Note ve AuditLog kısmı düzenlendi.

This commit is contained in:
Sedat Öztürk 2026-03-22 03:56:24 +03:00
parent 4943f78f89
commit 203160fce0
13 changed files with 756 additions and 190 deletions

View file

@ -15,6 +15,8 @@ public class AuditLogDto : EntityDto<Guid>
public string ApplicationName { get; set; } public string ApplicationName { get; set; }
public Guid? UserId { get; protected set; } public Guid? UserId { get; protected set; }
public string UserName { 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 DateTime ExecutionTime { get; protected set; }
public int ExecutionDuration { get; protected set; } public int ExecutionDuration { get; protected set; }
public string ClientIpAddress { get; protected set; } public string ClientIpAddress { get; protected set; }

View file

@ -306,7 +306,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
}; };
var queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value); 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 // Performans: Dictionary ile hızlı lookup
var columnFormatsDict = result.ColumnFormats.ToDictionary(c => c.FieldName, c => c); var columnFormatsDict = result.ColumnFormats.ToDictionary(c => c.FieldName, c => c);

View file

@ -1008,12 +1008,30 @@
"en": "Cancel", "en": "Cancel",
"tr": "İptal" "tr": "İptal"
}, },
{
"resourceName": "Platform",
"key": "Download",
"en": "Download",
"tr": "İndir"
},
{
"resourceName": "Platform",
"key": "Insert",
"en": "Insert",
"tr": "Ekle"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "Delete", "key": "Delete",
"en": "Delete", "en": "Delete",
"tr": "Sil" "tr": "Sil"
}, },
{
"resourceName": "Platform",
"key": "Operation",
"en": "Operation",
"tr": "İşlem"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Platform.HangfireLogin", "key": "App.Platform.HangfireLogin",
@ -3270,6 +3288,90 @@
"en": "Refresh", "en": "Refresh",
"tr": "Tazele" "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", "resourceName": "Platform",
"key": "ListForms.ListForm.DeleteFilter", "key": "ListForms.ListForm.DeleteFilter",

View file

@ -1722,7 +1722,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Sector)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Sector)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{ {
@ -1815,7 +1815,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.WorkHour)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.WorkHour)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {

View file

@ -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"; 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 = "CreationTime", FieldDbType = DbType.DateTime, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", 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 = "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[] public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid) => JsonSerializer.Serialize(new FieldsDefaultValue[]

View file

@ -1478,6 +1478,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Currency)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Currency)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1485,14 +1486,13 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new() { new() {
Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group", Items = 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 = 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 = 3, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 4, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox }, new EditingFormItemDto { Order = 4, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
] ]
} }
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
}); });
#region Currency Fields #region Currency Fields
@ -1623,6 +1623,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CountryGroup)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CountryGroup)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1634,7 +1635,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
] ]
} }
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
}); });
#region CountryGroup Fields #region CountryGroup Fields
@ -1713,6 +1713,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Country)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Country)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1730,7 +1731,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
] ]
} }
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Currency", FieldDbType = DbType.String, Value = "TRY", CustomValueType = FieldCustomValueTypeEnum.Value } new() { FieldName = "Currency", FieldDbType = DbType.String, Value = "TRY", CustomValueType = FieldCustomValueTypeEnum.Value }
}) })
@ -2210,6 +2210,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.UomCategory)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.UomCategory)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, false, false, true), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, false, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -2221,7 +2222,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
] ]
} }
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() { SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() {
new { new {
TabType = ListFormTabTypeEnum.List, TabType = ListFormTabTypeEnum.List,
@ -2316,6 +2316,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Uom)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Uom)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
@ -2328,7 +2329,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2=EditorTypes.dxCheckBox }, new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2=EditorTypes.dxCheckBox },
]} ]}
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value } 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)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillType)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false, true), EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto> EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{ {
@ -2638,7 +2638,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillLevel)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillLevel)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items= 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)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Skill)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(AppCodes.Definitions.SkillLevel, 600, 300, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(AppCodes.Definitions.SkillLevel, 600, 300, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ 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)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.MenuGroup)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
}); });
#region MenuGroup Fields #region MenuGroup Fields
@ -5217,7 +5217,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 12, DataField = "ShortName", ColSpan = 1, EditorType2=EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 12, DataField = "ShortName", ColSpan = 1, EditorType2=EditorTypes.dxTextBox },
]} ]}
}), }),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Code"),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsDisabled", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value } 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)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.PaymentMethod)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value } new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value }
}), }),

View file

@ -106,7 +106,7 @@ public class ListFormManager : PlatformDomainService, IListFormManager
} }
// ListFormField icerisinde olmayan (select sorgusu ile alinmayan) deger almasi gereken zorunlu alanlar // 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) if (PlatformConsts.IsMultiTenant && listForm.IsTenant)
{ {

View file

@ -20,9 +20,11 @@ public interface IDefaultValueManager
{ {
Task<Dictionary<string, object>> GenerateDefaultValuesAsync( Task<Dictionary<string, object>> GenerateDefaultValuesAsync(
ListForm listForm, ListForm listForm,
List<ListFormField> listFormFields,
OperationEnum op, OperationEnum op,
object[] keys = null, object[] keys = null,
Dictionary<string, StringValues> queryParameters = null); Dictionary<string, StringValues> queryParameters = null,
dynamic inputParams = null);
} }
public class DefaultValueManager : PlatformDomainService, IDefaultValueManager public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
@ -39,9 +41,11 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
public async Task<Dictionary<string, object>> GenerateDefaultValuesAsync( public async Task<Dictionary<string, object>> GenerateDefaultValuesAsync(
ListForm listForm, ListForm listForm,
List<ListFormField> listFormFields,
OperationEnum op, OperationEnum op,
object[] keys = null, object[] keys = null,
Dictionary<string, StringValues> queryParameters = null) Dictionary<string, StringValues> queryParameters = null,
dynamic inputParams = null)
{ {
var fields = new Dictionary<string, object>(); var fields = new Dictionary<string, object>();
@ -60,69 +64,87 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
return fields; return fields;
} }
foreach (var field in JsonSerializer.Deserialize<List<FieldsDefaultValue>>(defaultFieldsJson)) foreach (var defaultField in JsonSerializer.Deserialize<List<FieldsDefaultValue>>(defaultFieldsJson))
{ {
object value = null; object value = null;
switch (field.CustomValueType) switch (defaultField.CustomValueType)
{ {
case FieldCustomValueTypeEnum.CustomKey: case FieldCustomValueTypeEnum.CustomKey:
if (field.Value == PlatformConsts.DefaultValues.UserId) if (defaultField.Value == PlatformConsts.DefaultValues.UserId)
value = CurrentUser.Id; value = CurrentUser.Id;
else if (field.Value == PlatformConsts.DefaultValues.UserName) else if (defaultField.Value == PlatformConsts.DefaultValues.UserName)
value = CurrentUser.Name; value = CurrentUser.Name;
else if (field.Value == PlatformConsts.DefaultValues.Roles) else if (defaultField.Value == PlatformConsts.DefaultValues.Roles)
value = CurrentUser.Roles; //.JoinAsString("','"); value = CurrentUser.Roles; //.JoinAsString("','");
else if (field.Value == PlatformConsts.DefaultValues.Date) else if (defaultField.Value == PlatformConsts.DefaultValues.Date)
value = Clock.Now.Date; value = Clock.Now.Date;
else if (field.Value == PlatformConsts.DefaultValues.Now) else if (defaultField.Value == PlatformConsts.DefaultValues.Now)
value = Clock.Now; value = Clock.Now;
else if (field.Value == PlatformConsts.DefaultValues.Day) else if (defaultField.Value == PlatformConsts.DefaultValues.Day)
value = Clock.Now.Day; value = Clock.Now.Day;
else if (field.Value == PlatformConsts.DefaultValues.Month) else if (defaultField.Value == PlatformConsts.DefaultValues.Month)
value = Clock.Now.Month; value = Clock.Now.Month;
else if (field.Value == PlatformConsts.DefaultValues.Year) else if (defaultField.Value == PlatformConsts.DefaultValues.Year)
value = Clock.Now.Year; value = Clock.Now.Year;
else if (field.Value == PlatformConsts.DefaultValues.Id) else if (defaultField.Value == PlatformConsts.DefaultValues.Id)
value = keys?.FirstOrDefault(); value = keys?.FirstOrDefault();
else if (field.Value == PlatformConsts.DefaultValues.NewId) else if (defaultField.Value == PlatformConsts.DefaultValues.NewId)
value = Guid.NewGuid(); value = Guid.NewGuid();
else if (field.Value == PlatformConsts.DefaultValues.Selected_Ids) else if (defaultField.Value == PlatformConsts.DefaultValues.Selected_Ids)
value = keys; value = keys;
else if (field.Value == PlatformConsts.DefaultValues.TenantId) else if (defaultField.Value == PlatformConsts.DefaultValues.TenantId)
value = CurrentTenant.Id; value = CurrentTenant.Id;
else else
value = field.Value; {
if ((object)inputParams != null)
{
foreach (var item in JsonSerializer.Deserialize<Dictionary<string, object>>(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 //TODO: artirilabilir
break; break;
case FieldCustomValueTypeEnum.QueryParams: case FieldCustomValueTypeEnum.QueryParams:
if (queryParameters != null) if (queryParameters != null)
{ {
value = queryParameters.GetValueOrDefault(field.Value); value = queryParameters.GetValueOrDefault(defaultField.Value);
} }
break; break;
case FieldCustomValueTypeEnum.DbQuery: case FieldCustomValueTypeEnum.DbQuery:
if (!string.IsNullOrWhiteSpace(field.SqlQuery)) if (!string.IsNullOrWhiteSpace(defaultField.SqlQuery))
{ {
var dynamicDataManager = LazyServiceProvider.LazyGetRequiredService<IDynamicDataManager>(); var dynamicDataManager = LazyServiceProvider.LazyGetRequiredService<IDynamicDataManager>();
var (dynamicDataRepository, connectionString, _) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); var (dynamicDataRepository, connectionString, _) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
value = await dynamicDataRepository.ExecuteScalarAsync<object>(field.SqlQuery, connectionString); value = await dynamicDataRepository.ExecuteScalarAsync<object>(defaultField.SqlQuery, connectionString);
} }
break; break;
case FieldCustomValueTypeEnum.Value: case FieldCustomValueTypeEnum.Value:
default: default:
value = field.Value; value = defaultField.Value;
break; break;
} }
if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
{ {
var formattedValue = QueryHelper.GetFormattedValue(field.FieldDbType, value); var formattedValue = QueryHelper.GetFormattedValue(defaultField.FieldDbType, value);
if (fields.ContainsKey(field.FieldName)) if (fields.ContainsKey(defaultField.FieldName))
{ {
fields[field.FieldName] = formattedValue; fields[defaultField.FieldName] = formattedValue;
} }
else else
{ {
fields.Add(field.FieldName, formattedValue); fields.Add(defaultField.FieldName, formattedValue);
} }
} }
} }

View file

@ -26,6 +26,8 @@ export interface AuditLogDto {
applicationName: string applicationName: string
userId?: string userId?: string
userName?: string userName?: string
tenantId?: string
tenantName?: string
executionTime: string executionTime: string
executionDuration: number executionDuration: number
clientIpAddress?: string clientIpAddress?: string

View file

@ -7,7 +7,7 @@ import {
UserClaimModel, UserClaimModel,
UserInfoViewModel, UserInfoViewModel,
} from '@/proxy/admin/models' } from '@/proxy/admin/models'
import { ListResultDto } from '../proxy' import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '../proxy'
import { AuditLogDto } from '../proxy/auditLog/audit-log' import { AuditLogDto } from '../proxy/auditLog/audit-log'
import apiService from './api.service' import apiService from './api.service'
@ -74,6 +74,13 @@ export const getAuditLogs = (id: string) =>
url: `/api/app/audit-log/${id}`, url: `/api/app/audit-log/${id}`,
}) })
export const getAuditLogList = (input: PagedAndSortedResultRequestDto) =>
apiService.fetchData<PagedResultDto<AuditLogDto>, PagedAndSortedResultRequestDto>({
method: 'GET',
url: '/api/app/audit-log',
params: input,
})
export const postClaimUser = (input: UserClaimModel) => export const postClaimUser = (input: UserClaimModel) =>
apiService.fetchData({ apiService.fetchData({
method: 'POST', method: 'POST',

View file

@ -1,22 +1,62 @@
import React from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { FaStickyNote, FaEnvelope, FaTrash, FaDownload, FaClock, FaPaperclip } from 'react-icons/fa' import {
import { Avatar, Button } from '@/components/ui' 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 { NoteDto } from '@/proxy/note/models'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { useStoreState } from '@/store/store' 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 { interface NoteListProps {
notes: NoteDto[] notes: NoteDto[]
entityName: string
entityId: string
onAddNote?: () => void
onDeleteNote?: (noteId: string) => void onDeleteNote?: (noteId: string) => void
onDownloadFile?: (fileData: any) => void onDownloadFile?: (fileData: any) => void
} }
export const NoteList: React.FC<NoteListProps> = ({ export const NoteList: React.FC<NoteListProps> = ({
notes, notes,
entityName,
entityId,
onAddNote,
onDeleteNote, onDeleteNote,
onDownloadFile, onDownloadFile,
}) => { }) => {
const user = useStoreState((state) => state.auth.user) 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<string | null>(null)
const [auditItems, setAuditItems] = useState<
Array<{
log: AuditLogDto
matchedActions: Array<{
action: AuditLogActionDto
input: any
operation: string
rowLabel?: string
}>
}>
>([])
const [auditLoadedKey, setAuditLoadedKey] = useState<string | null>(null)
const getNoteStyle = (type: string) => { const getNoteStyle = (type: string) => {
switch (type) { switch (type) {
@ -38,16 +78,289 @@ export const NoteList: React.FC<NoteListProps> = ({
} }
} }
if (notes.length === 0) const entityIdNormalized = useMemo(
return ( () =>
<div className="flex flex-col items-center justify-center h-32 text-gray-500"> String(entityId ?? '')
<FaStickyNote className="text-4xl mb-2 opacity-50" /> .trim()
<p className="text-sm">Henüz hiçbir not bulunmuyor</p> .toLowerCase(),
</div> [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<any>,
): { 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<any>()
if (visit.has(data)) return null
visit.add(data)
const entries = Object.entries(data as Record<string, any>)
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<PagedResultDto<AuditLogDto>>({
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 <Badge className="bg-gray-500" content="?" />
if (statusCode >= 200 && statusCode < 300)
return <Badge className="bg-green-500" content={statusCode} />
if (statusCode >= 400 && statusCode < 500)
return <Badge className="bg-yellow-500" content={statusCode} />
if (statusCode >= 500) return <Badge className="bg-red-500" content={statusCode} />
return <Badge className="bg-blue-500" content={statusCode} />
}
return ( return (
<div className="relative"> <div className="relative">
<Tabs
value={currentTab}
onChange={(val) => setCurrentTab(val as 'notes' | 'audit')}
variant="pill"
>
<TabList className="mb-4 bg-gray-50 p-1 rounded-lg">
<TabNav value="notes">
{translate('::ListForms.ListForm.Notes')}
<Badge className="ml-2 bg-blue-500" content={`${notes?.length ?? 0}`} />
</TabNav>
<TabNav value="audit">
{translate('::App.AuditLogs')}
<Badge className="ml-2 bg-purple-500" content={`${auditItems?.length ?? 0}`} />
</TabNav>
</TabList>
<TabContent value="notes">
<div className="flex items-center justify-end mb-2">
<Button
variant="solid"
size="xs"
type="button"
onClick={onAddNote}
disabled={!onAddNote}
className="flex items-center"
>
<FaPlus className="mr-1" /> {translate('::ListForms.ListForm.AddNote')}
</Button>
</div>
{(notes?.length ?? 0) === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
<FaStickyNote className="text-4xl mb-2 opacity-50" />
<p className="text-sm">{translate('::ListForms.ListForm.Notes.Empty')}</p>
</div>
) : (
<div className="space-y-5 ml-5"> <div className="space-y-5 ml-5">
{notes.map((note, index) => { {notes.map((note, index) => {
const files = note.filesJson ? JSON.parse(note.filesJson) : [] const files = note.filesJson ? JSON.parse(note.filesJson) : []
@ -89,7 +402,7 @@ export const NoteList: React.FC<NoteListProps> = ({
variant="plain" variant="plain"
size="xs" size="xs"
onClick={() => onDeleteNote?.(note.id as string)} onClick={() => onDeleteNote?.(note.id as string)}
title="Sil" title={translate('::Delete')}
className="text-red-400 hover:text-red-600" className="text-red-400 hover:text-red-600"
> >
<FaTrash /> <FaTrash />
@ -102,9 +415,7 @@ export const NoteList: React.FC<NoteListProps> = ({
{note.subject && ( {note.subject && (
<h4 className="text-sm font-bold text-gray-900 mb-1">{note.subject}</h4> <h4 className="text-sm font-bold text-gray-900 mb-1">{note.subject}</h4>
)} )}
{note.content && ( {note.content && <div dangerouslySetInnerHTML={{ __html: note.content }} />}
<div dangerouslySetInnerHTML={{ __html: note.content }} />
)}
</div> </div>
{/* Files */} {/* Files */}
@ -121,7 +432,7 @@ export const NoteList: React.FC<NoteListProps> = ({
variant="plain" variant="plain"
size="xs" size="xs"
onClick={() => onDownloadFile?.(file)} onClick={() => onDownloadFile?.(file)}
title="İndir" title={translate('::Download')}
className="text-blue-500 hover:text-blue-700 ml-1" className="text-blue-500 hover:text-blue-700 ml-1"
> >
<FaDownload /> <FaDownload />
@ -135,6 +446,143 @@ export const NoteList: React.FC<NoteListProps> = ({
) )
})} })}
</div> </div>
)}
</TabContent>
<TabContent value="audit">
<div className="flex items-center justify-end mb-2">
<Button
size="xs"
variant="default"
type="button"
onClick={loadAuditLogs}
disabled={auditLoading}
className="flex items-center"
>
<FaSyncAlt className="mr-1" /> {translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
{auditLoading ? (
<div className="flex items-center justify-center py-10">
<Spinner size={32} />
</div>
) : auditError ? (
<div className="text-sm text-red-600">{auditError}</div>
) : (auditItems?.length ?? 0) === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
<FaHistory className="text-4xl mb-2 opacity-50" />
<p className="text-sm">{translate('::ListForms.ListForm.AuditLogs.Empty')}</p>
</div>
) : (
<div className="space-y-4 ml-5">
{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<string, any>)
for (const [k, v] of entries) {
changeLines.push(`${k}: ${String(v)}`)
}
}
return (
<div
key={log.id}
className="relative bg-white border-l-4 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 border-purple-400"
>
<div className="absolute -left-7 top-4 bg-white rounded-full border-2 border-gray-300 p-2">
<FaHistory className="text-purple-600" />
</div>
<div className="p-4">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-1 text-sm font-semibold text-gray-800">
<Avatar
size={25}
shape="circle"
src={AVATAR_URL(log.userId, log.tenantId)}
/>
{log.userName}
</div>
{execTime && (
<div className="flex items-center gap-1 text-xs text-gray-400 mt-1">
<FaClock /> {execTime.toLocaleString()}
</div>
)}
<div className="flex items-center gap-2 mt-2 flex-wrap">
<Badge className={opClass} content={opLabel} />
{rowLabel ? <Badge className="bg-gray-600" content={rowLabel} /> : null}
</div>
<div className="text-xs text-gray-500 mt-1 truncate">
{(log.httpMethod || 'HTTP') + ' ' + (log.url || '')}
</div>
{changeLines.length > 0 && (
<div className="mt-2 text-xs text-gray-600 space-y-1">
{changeLines.slice(0, 8).map((line, idx) => (
<div key={idx} className="truncate">
{line}
</div>
))}
{changeLines.length > 8 && (
<div className="text-gray-400">
+{changeLines.length - 8} değişiklik daha
</div>
)}
</div>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{getStatusBadge(log.httpStatusCode)}
<Badge className="bg-purple-500" content={`${changeCount}`} />
</div>
</div>
</div>
</div>
)
})}
</div>
)}
</TabContent>
</Tabs>
</div> </div>
) )
} }

View file

@ -13,11 +13,12 @@ import {
headerOptions, headerOptions,
} from '@/proxy/reports/data' } from '@/proxy/reports/data'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor' import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import { useLocalization } from '@/utils/hooks/useLocalization'
const validationSchema = Yup.object({ const validationSchema = Yup.object({
type: Yup.string().required('Not tipi zorunludur'), type: Yup.string().required(),
subject: Yup.string().required('Konu zorunludur'), subject: Yup.string().required(),
content: Yup.string().required('İçerik zorunludur'), content: Yup.string().required(),
}) })
interface NoteModalProps { interface NoteModalProps {
@ -37,11 +38,11 @@ export const NoteModal: React.FC<NoteModalProps> = ({
}) => { }) => {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [fileList, setFileList] = useState<File[]>([]) const [fileList, setFileList] = useState<File[]>([])
const { translate } = useLocalization()
const types = [ const types = [
{ value: 'note', label: 'Not' }, { value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') },
{ value: 'message', label: 'Mesaj' }, { value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') },
{ value: 'activity', label: 'Aktivite' }, { value: 'activity', label: translate('::ListForms.ListForm.NoteModal.Type.Activity') },
] ]
const handleSave = async (values: any) => { const handleSave = async (values: any) => {
@ -69,7 +70,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
const beforeUpload = (files: FileList | null) => { const beforeUpload = (files: FileList | null) => {
if (!files) return true if (!files) return true
for (const f of Array.from(files)) { 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 return true
} }
@ -87,7 +88,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
<div className="p-2 bg-purple-100 rounded-full"> <div className="p-2 bg-purple-100 rounded-full">
<FaPlus className="text-purple-600 text-lg" /> <FaPlus className="text-purple-600 text-lg" />
</div> </div>
Not / Aktivite Ekle {translate('::ListForms.ListForm.AddNote')}
</h3> </h3>
</div> </div>
@ -104,11 +105,11 @@ export const NoteModal: React.FC<NoteModalProps> = ({
invalid={!!(errors.type && touched.type)} invalid={!!(errors.type && touched.type)}
errorMessage={errors.type} errorMessage={errors.type}
> >
<div className="flex gap-4"> <div className="flex gap-2">
{types.map((t) => ( {types.map((t) => (
<label <label
key={t.value} key={t.value}
className={`flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer transition-all duration-200 ${ className={`flex items-center gap-2 px-2 py-1 rounded-md cursor-pointer transition-all duration-200 ${
values.type === t.value values.type === t.value
? 'border-purple-500 bg-purple-50 text-purple-700' ? 'border-purple-500 bg-purple-50 text-purple-700'
: 'border-gray-300 hover:border-purple-400' : 'border-gray-300 hover:border-purple-400'
@ -128,6 +129,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
{/* KONUSU */} {/* KONUSU */}
<FormItem <FormItem
label="Konu" label="Konu"
asterisk
invalid={!!(errors.subject && touched.subject)} invalid={!!(errors.subject && touched.subject)}
errorMessage={errors.subject} errorMessage={errors.subject}
> >
@ -135,7 +137,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
type="text" type="text"
name="subject" name="subject"
as={Input} as={Input}
placeholder="Kısa bir başlık girin..." placeholder={translate('::ListForms.ListForm.NoteModal.Subject')}
/> />
</FormItem> </FormItem>
@ -152,7 +154,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
value={field.value} value={field.value}
onValueChanged={(e) => setFieldValue('content', e.value)} onValueChanged={(e) => setFieldValue('content', e.value)}
height={220} height={220}
placeholder="Notunuzu buraya yazın..." placeholder={translate('::ListForms.ListForm.NoteModal.Content')}
> >
<MediaResizing enabled={true} /> <MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" /> <ImageUpload fileUploadMode="base64" />
@ -200,7 +202,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
</FormItem> </FormItem>
{/* DOSYA YÜKLEME */} {/* DOSYA YÜKLEME */}
<FormItem label="Dosya Ekle"> <FormItem>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200"> <div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
<Upload <Upload
className="cursor-pointer" className="cursor-pointer"
@ -223,7 +225,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
variant="twoTone" variant="twoTone"
className="flex items-center justify-center mx-auto" className="flex items-center justify-center mx-auto"
> >
Dosya Yükle {translate('::App.Listforms.ImportManager.UploadFile')}
</Button> </Button>
</Upload> </Upload>
@ -261,7 +263,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
{/* ALT BUTONLAR */} {/* ALT BUTONLAR */}
<div className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200"> <div className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200">
<Button variant="default" size="md" onClick={onClose} disabled={uploading}> <Button variant="default" size="md" onClick={onClose} disabled={uploading}>
İptal {translate('::Cancel')}
</Button> </Button>
<Button <Button
variant="solid" variant="solid"
@ -270,7 +272,7 @@ export const NoteModal: React.FC<NoteModalProps> = ({
disabled={isSubmitting || !values.subject || !values.type} disabled={isSubmitting || !values.subject || !values.type}
className="px-6 flex items-center gap-2" className="px-6 flex items-center gap-2"
> >
{uploading ? 'Yükleniyor...' : 'Ekle'} {uploading ? translate('::App.Loading') : translate('::ListForms.Wizard.Add')}
</Button> </Button>
</div> </div>
</Form> </Form>

View file

@ -5,14 +5,12 @@ import { Button, Badge } from '@/components/ui'
import { import {
FaChevronLeft, FaChevronLeft,
FaChevronRight, FaChevronRight,
FaPlus,
FaTimes, FaTimes,
FaGripVertical, FaGripVertical,
FaChevronUp,
FaChevronDown,
} from 'react-icons/fa' } from 'react-icons/fa'
import { noteService } from '@/services/note.service' import { noteService } from '@/services/note.service'
import { NoteDto } from '@/proxy/note/models' import { NoteDto } from '@/proxy/note/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface NotePanelProps { interface NotePanelProps {
entityName: string entityName: string
@ -34,6 +32,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 }) const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
const buttonRef = useRef<HTMLDivElement>(null) const buttonRef = useRef<HTMLDivElement>(null)
const [showEntityInfo, setShowEntityInfo] = useState(false) const [showEntityInfo, setShowEntityInfo] = useState(false)
const { translate } = useLocalization()
// Fetch activities // Fetch activities
const fetchActivities = async () => { const fetchActivities = async () => {
@ -58,12 +57,12 @@ export const NotePanel: React.FC<NotePanelProps> = ({
fileData.FileType, fileData.FileType,
) )
} catch (err) { } catch (err) {
console.error('Dosya indirilemedi', err) console.error(translate('::ListForms.ListForm.NotesPanel.DownloadFailed'), err)
} }
} }
const handleDeleteActivity = async (activityId: string) => { const handleDeleteActivity = async (activityId: string) => {
if (!confirm('Bu aktiviteyi silmek istediğinize emin misiniz?')) return if (!confirm(translate('::DeleteConfirmation'))) return
try { try {
await noteService.delete(activityId) await noteService.delete(activityId)
setActivities((prev) => prev.filter((a) => a.id !== activityId)) setActivities((prev) => prev.filter((a) => a.id !== activityId))
@ -136,7 +135,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
e.stopPropagation() e.stopPropagation()
onToggle() onToggle()
}} }}
title={isVisible ? 'Paneli kapat' : 'Paneli aç'} title={isVisible ? translate('::ListForms.ListForm.NotesPanel.ClosePanel') : translate('::ListForms.ListForm.NotesPanel.OpenPanel')}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isVisible ? <FaChevronRight /> : <FaChevronLeft />} {isVisible ? <FaChevronRight /> : <FaChevronLeft />}
@ -161,19 +160,14 @@ export const NotePanel: React.FC<NotePanelProps> = ({
<div className="p-4 border-b border-gray-200 bg-gray-50"> <div className="p-4 border-b border-gray-200 bg-gray-50">
{/* Üst Satır: Başlık, Kayıt Bilgisi Toggle ve Kapat Butonu */} {/* Üst Satır: Başlık, Kayıt Bilgisi Toggle ve Kapat Butonu */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-800">Notlar</h3> <div className="flex items-center gap-1 text-sm text-gray-700">
<span className="font-medium">{entityName}</span>
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono">
<Badge className="bg-blue-100 text-blue-600" content={entityId} />
</code>
</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* 👇 Kayıt Bilgisi Aç/Kapa Butonu */}
<button
onClick={() => setShowEntityInfo((prev) => !prev)}
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 cursor-pointer select-none"
title="Kayıt Bilgisi"
>
{showEntityInfo ? <FaChevronUp /> : <FaChevronDown />}
</button>
{/* Kapat Butonu */}
<Button variant="plain" size="xs" onClick={onToggle} title="Paneli kapat"> <Button variant="plain" size="xs" onClick={onToggle} title="Paneli kapat">
<FaTimes /> <FaTimes />
</Button> </Button>
@ -186,30 +180,17 @@ export const NotePanel: React.FC<NotePanelProps> = ({
showEntityInfo ? 'max-h-20 mt-2 opacity-100' : 'max-h-0 opacity-0' showEntityInfo ? 'max-h-20 mt-2 opacity-100' : 'max-h-0 opacity-0'
}`} }`}
> >
<div className="flex items-center gap-1 text-sm text-gray-700">
<span className="font-medium">{entityName}</span>
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono">
<Badge className="bg-blue-100 text-blue-600" content={entityId} />
</code>
</div>
</div> </div>
{/* Alt buton: Not Ekle */}
<div className="flex gap-2 mt-3">
<Button
variant="solid"
size="xs"
onClick={() => setShowAddModal(true)}
className="flex justify-center items-center py-4 w-full"
>
<FaPlus className="mr-1" /> Not Ekle
</Button>
</div>
</div> </div>
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<NoteList <NoteList
notes={activities} notes={activities}
entityName={entityName}
entityId={entityId}
onAddNote={() => setShowAddModal(true)}
onDeleteNote={handleDeleteActivity} onDeleteNote={handleDeleteActivity}
onDownloadFile={handleDownloadFile} onDownloadFile={handleDownloadFile}
/> />