diff --git a/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogListRequestDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogListRequestDto.cs new file mode 100644 index 0000000..e0cc568 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/AuditLogs/AuditLogListRequestDto.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Application.Dtos; + +namespace Sozsoft.Platform.AuditLogs; + +public class AuditLogListRequestDto : PagedAndSortedResultRequestDto +{ + public string ListFormCode { get; set; } + public string EntityId { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application/AuditLogs/AuditLogAppService.cs b/api/src/Sozsoft.Platform.Application/AuditLogs/AuditLogAppService.cs index 8c6800e..2979160 100644 --- a/api/src/Sozsoft.Platform.Application/AuditLogs/AuditLogAppService.cs +++ b/api/src/Sozsoft.Platform.Application/AuditLogs/AuditLogAppService.cs @@ -1,30 +1,43 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.ListForms; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.AuditLogging; +using Volo.Abp.Domain.Repositories; using Volo.Abp.Uow; using static Sozsoft.Platform.Data.Seeds.SeedConsts; namespace Sozsoft.Platform.AuditLogs; public interface IAuditLogAppService - : ICrudAppService + : ICrudAppService { } [Authorize(AppCodes.IdentityManagement.AuditLogs)] -public class AuditLogAppService - : CrudAppService - , IAuditLogAppService +public class AuditLogAppService : CrudAppService< + AuditLog, + AuditLogDto, + Guid, + AuditLogListRequestDto>, IAuditLogAppService { - public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository) + private readonly IRepository _listFormRepository; + + public AuditLogAppService( + IAuditLogRepository auditLogRepository, + IRepository listFormRepository + ) : base(auditLogRepository) { + _listFormRepository = listFormRepository; } public override async Task GetAsync(Guid id) @@ -35,27 +48,30 @@ public class AuditLogAppService } [UnitOfWork] - public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) + public override async Task> GetListAsync(AuditLogListRequestDto input) { - var query = await CreateFilteredQueryAsync(input); + var query = await Repository.WithDetailsAsync(); + + if (!input.ListFormCode.IsNullOrWhiteSpace()) + { + var filterRules = await GetListFormFilterRulesAsync(input.ListFormCode); + query = ApplyAuditLogActionParametersFilter(query, filterRules, input.EntityId); + } + else if (!input.EntityId.IsNullOrWhiteSpace()) + { + query = query.Where(a => a.Actions.Any(action => action.Parameters.Contains(input.EntityId))); + } var totalCount = await AsyncExecuter.CountAsync(query); query = ApplySorting(query, input); query = ApplyPaging(query, input); - // EntityChanges ile birlikte getir (N+1 query önlenir) - var auditLogRepository = (IAuditLogRepository)Repository; - var auditLogsWithDetails = await auditLogRepository.GetListAsync( - sorting: input.Sorting, - maxResultCount: input.MaxResultCount, - skipCount: input.SkipCount, - includeDetails: true - ); + var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query); // Mapping tek seferde yap var entityDtos = ObjectMapper.Map, List>(auditLogsWithDetails); - + // EntityChangeCount'u doldur (artık EntityChanges yüklü) foreach (var dto in entityDtos) { @@ -69,6 +85,102 @@ public class AuditLogAppService ); } + private async Task> GetListFormFilterRulesAsync(string listFormCode) + { + var rules = new List + { + new(listFormCode, null) + }; + + var listForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); + if (listForm?.SubFormsJson.IsNullOrWhiteSpace() != false) + { + return rules; + } + + try + { + var subForms = JsonSerializer.Deserialize>(listForm.SubFormsJson) ?? []; + foreach (var subForm in subForms.Where(a => !a.Code.IsNullOrWhiteSpace())) + { + var childFieldNames = subForm.Relation? + .Select(a => a.ChildFieldName) + .Where(a => !a.IsNullOrWhiteSpace()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList() ?? []; + + rules.AddRange(childFieldNames.Select(childFieldName => new AuditLogListFormFilterRule(subForm.Code, childFieldName))); + } + } + catch (JsonException) + { + // Invalid subform JSON should not block audit log listing for the main form. + } + + return rules + .DistinctBy(a => $"{a.ListFormCode}|{a.ChildFieldName}".ToLowerInvariant()) + .ToList(); + } + + private static IQueryable ApplyAuditLogActionParametersFilter( + IQueryable query, + List rules, + string entityId) + { + var validRules = rules + .Where(rule => !rule.ListFormCode.IsNullOrWhiteSpace()) + .ToList(); + + if (validRules.Count == 0) + { + return query; + } + + var auditLog = Expression.Parameter(typeof(AuditLog), "auditLog"); + var action = Expression.Parameter(typeof(AuditLogAction), "action"); + var parameters = Expression.Property(action, nameof(AuditLogAction.Parameters)); + + Expression actionBody = Expression.Constant(false); + foreach (var rule in validRules) + { + Expression ruleBody = Contains(parameters, rule.ListFormCode); + if (!entityId.IsNullOrWhiteSpace()) + { + ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, entityId)); + } + + if (!rule.ChildFieldName.IsNullOrWhiteSpace()) + { + ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, rule.ChildFieldName)); + } + + actionBody = Expression.OrElse(actionBody, ruleBody); + } + + var actions = Expression.Property(auditLog, nameof(AuditLog.Actions)); + var actionPredicate = Expression.Lambda>(actionBody, action); + var anyCall = Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Any), + [typeof(AuditLogAction)], + actions, + actionPredicate); + + var auditLogPredicate = Expression.Lambda>(anyCall, auditLog); + return query.Where(auditLogPredicate); + } + + private static MethodCallExpression Contains(MemberExpression source, string value) + { + return Expression.Call( + source, + nameof(string.Contains), + Type.EmptyTypes, + Expression.Constant(value)); + } + + private sealed record AuditLogListFormFilterRule(string ListFormCode, string? ChildFieldName); + // Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz [RemoteService(IsEnabled = false)] public override Task CreateAsync(AuditLogDto input) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs index bc59dfd..d937ff2 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs @@ -2302,7 +2302,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, false, false), EditingFormJson = JsonSerializer.Serialize(new List() { new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ diff --git a/ui/src/services/auditLog.service.ts b/ui/src/services/auditLog.service.ts new file mode 100644 index 0000000..f63dd00 --- /dev/null +++ b/ui/src/services/auditLog.service.ts @@ -0,0 +1,24 @@ +import { PagedResultDto } from '@/proxy' +import { AuditLogDto } from '@/proxy/auditLog/audit-log' +import apiService from '@/services/api.service' + +export interface AuditLogListRequestDto { + skipCount?: number + maxResultCount?: number + sorting?: string + listFormCode?: string + entityId?: string +} + +class AuditLogService { + async getList(params?: AuditLogListRequestDto): Promise> { + const response = await apiService.fetchData>({ + url: '/api/app/audit-log', + method: 'GET', + params, + }) + return response.data + } +} + +export const auditLogService = new AuditLogService() diff --git a/ui/src/views/form/notes/NoteList.tsx b/ui/src/views/form/notes/NoteList.tsx index 599c4e9..30378d7 100644 --- a/ui/src/views/form/notes/NoteList.tsx +++ b/ui/src/views/form/notes/NoteList.tsx @@ -17,16 +17,17 @@ 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' +import { auditLogService } from '@/services/auditLog.service' interface NoteListProps { notes: NoteDto[] entityName: string entityId: string + isVisible?: boolean onAddNote?: () => void + onRefreshNotes?: () => void onDeleteNote?: (noteId: string) => void onDownloadFile?: (fileData: any) => void } @@ -35,7 +36,9 @@ export const NoteList: React.FC = ({ notes, entityName, entityId, + isVisible = true, onAddNote, + onRefreshNotes, onDeleteNote, onDownloadFile, }) => { @@ -215,7 +218,18 @@ export const NoteList: React.FC = ({ const getRowLabelIfMatches = (input: any): string | null => { if (!entityIdNormalized) return null const inputFormCode = normalize(input?.listFormCode) - if (!inputFormCode || inputFormCode !== listFormCodeNormalized) return null + if (!inputFormCode) return null + const data = input?.data + const isMainListForm = inputFormCode === listFormCodeNormalized + + if (!isMainListForm) { + if (!data || typeof data !== 'object') return null + const hit = findMatchingValueInData(data, entityIdNormalized) + if (!hit) return null + + const nameValue = (data as any)?.Name ?? (data as any)?.name + return nameValue ? String(nameValue) : String(hit.value) + } const keys = getKeysFromInput(input) .map((k) => normalize(k)) @@ -227,7 +241,6 @@ export const NoteList: React.FC = ({ } // 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) { @@ -240,7 +253,6 @@ export const NoteList: React.FC = ({ } // 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 @@ -277,17 +289,15 @@ export const NoteList: React.FC = ({ 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 response = await auditLogService.getList({ + skipCount: 0, + maxResultCount: 200, + sorting: 'ExecutionTime DESC', + listFormCode: entityName, + entityId, }) - const items = response.data?.items ?? [] + const items = response?.items ?? [] const filtered = items .map((log) => ({ log, matchedActions: buildMatchedActions(log) })) .filter((x) => x.matchedActions.length > 0) @@ -305,13 +315,13 @@ export const NoteList: React.FC = ({ } useEffect(() => { - if (currentTab !== 'audit') return + if (!isVisible) 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]) + }, [isVisible, listFormCodeNormalized, entityIdNormalized]) const getStatusBadge = (statusCode?: number) => { if (!statusCode) return @@ -330,7 +340,7 @@ export const NoteList: React.FC = ({ onChange={(val) => setCurrentTab(val as 'notes' | 'audit')} variant="underline" > - + {translate('::ListForms.ListForm.Notes')} @@ -341,21 +351,48 @@ export const NoteList: React.FC = ({ - -
+
+ {currentTab === 'notes' ? ( +
+ + +
+ ) : ( -
+ )} +
+ {(notes?.length ?? 0) === 0 ? (
@@ -449,20 +486,6 @@ export const NoteList: React.FC = ({ -
- -
- {auditLoading ? (
diff --git a/ui/src/views/form/notes/NoteModal.tsx b/ui/src/views/form/notes/NoteModal.tsx index a365d09..0a9337c 100644 --- a/ui/src/views/form/notes/NoteModal.tsx +++ b/ui/src/views/form/notes/NoteModal.tsx @@ -92,7 +92,7 @@ function NoteModalContent({ {/* Başlık */}

-
+
{translate('::ListForms.ListForm.AddNote')} @@ -116,7 +116,7 @@ function NoteModalContent({ {types.map((t) => (