From ec44d03e79c6be6d43dfcee665d915dad7c0f3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 4 Feb 2026 21:25:35 +0300 Subject: [PATCH] =?UTF-8?q?IntranetAppService=20ve=20MenuAppService=20g?= =?UTF-8?q?=C3=BC=C3=A7lendirildi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Intranet/IntranetAppService.cs | 227 ++++++++++-------- .../Menu/MenuAppService.cs | 96 +++++--- ui/src/views/list/List.tsx | 6 +- 3 files changed, 182 insertions(+), 147 deletions(-) diff --git a/api/src/Erp.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Erp.Platform.Application/Intranet/IntranetAppService.cs index da9cbbc4..e4fd9a6f 100644 --- a/api/src/Erp.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Erp.Platform.Application/Intranet/IntranetAppService.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; namespace Erp.Platform.Public; @@ -83,8 +84,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _projectTaskRepository = projectTaskRepository; } + [UnitOfWork] public async Task GetIntranetDashboardAsync() { + // ABP 9.0.2: UnitOfWork kapsamında tüm sorgular optimize edilir return new IntranetDashboardDto { Events = await GetUpcomingEventsAsync(), @@ -107,21 +110,24 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private async Task> GetProjectTasksAsync() { - var tasks = await _projectTaskRepository - .WithDetailsAsync(e => e.Employee) - .ContinueWith(t => t.Result.Where(s => s.Priority == "High" || s.Priority == "Urgent") - .OrderByDescending(s => s.StartDate) - .Take(3) - .ToList()); + var queryable = await _projectTaskRepository.WithDetailsAsync(e => e.Employee); + + var tasks = await AsyncExecuter.ToListAsync( + queryable + .Where(s => s.Priority == "High" || s.Priority == "Urgent") + .OrderByDescending(s => s.StartDate) + .Take(3) + ); return ObjectMapper.Map, List>(tasks); } private async Task> GetSocialPostsAsync() { - var socialPosts = await _socialPostRepository - .WithDetailsAsync(e => e.Employee, e => e.Location, e => e.Media, e => e.Comments, e => e.Likes) - .ContinueWith(t => t.Result.ToList()); + var queryable = await _socialPostRepository + .WithDetailsAsync(e => e.Employee, e => e.Location, e => e.Media, e => e.Comments, e => e.Likes); + + var socialPosts = await AsyncExecuter.ToListAsync(queryable); return ObjectMapper.Map, List>(socialPosts); } @@ -130,29 +136,28 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService { var queryable = await _surveyRepository.GetQueryableAsync(); - var surveys = queryable - .Where(s => s.Status == "active") - .Include(s => s.Questions) - .ThenInclude(q => q.Options) - .ToList(); + var surveys = await AsyncExecuter.ToListAsync( + queryable + .Where(s => s.Status == "active") + .Include(s => s.Questions) + .ThenInclude(q => q.Options) + ); return ObjectMapper.Map, List>(surveys); } private async Task> GetOvertimesAsync() { - var overtimes = await _overtimeRepository - .WithDetailsAsync(e => e.Employee) - .ContinueWith(t => t.Result.ToList()); + var queryable = await _overtimeRepository.WithDetailsAsync(e => e.Employee); + var overtimes = await AsyncExecuter.ToListAsync(queryable); return ObjectMapper.Map, List>(overtimes); } private async Task> GetLeavesAsync() { - var leaves = await _leaveRepository - .WithDetailsAsync(e => e.Employee) - .ContinueWith(t => t.Result.ToList()); + var queryable = await _leaveRepository.WithDetailsAsync(e => e.Employee); + var leaves = await AsyncExecuter.ToListAsync(queryable); return ObjectMapper.Map, List>(leaves); } @@ -225,14 +230,14 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private async Task> GetAnnouncementsAsync() { - var announcements = await _announcementRepository - .WithDetailsAsync(e => e.Employee) - .ContinueWith(t => t.Result.ToList()); + var queryable = await _announcementRepository.WithDetailsAsync(e => e.Employee); + var announcements = await AsyncExecuter.ToListAsync(queryable); var announcementDtos = new List(); // Tüm departmanları bir kez çek (performans için) var allDepartments = await _departmentRepository.GetListAsync(); + var departmentDict = allDepartments.ToDictionary(d => d.Id, d => d.Name); foreach (var announcement in announcements) { @@ -246,21 +251,15 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService .Select(d => d.Trim()) .ToArray(); - // ID'leri Department Name'lere çevir - var departmentNames = new List(); - foreach (var deptId in departmentIds) - { - if (Guid.TryParse(deptId, out var guid)) - { - var department = allDepartments.FirstOrDefault(d => d.Id == guid); - if (department != null) - { - departmentNames.Add(department.Name); - } - } - } + // ID'leri Department Name'lere çevir (Dictionary lookup çok hızlı) + var departmentNames = departmentIds + .Where(deptId => Guid.TryParse(deptId, out _)) + .Select(deptId => Guid.Parse(deptId)) + .Where(guid => departmentDict.ContainsKey(guid)) + .Select(guid => departmentDict[guid]) + .ToArray(); - dto.Departments = departmentNames.ToArray(); + dto.Departments = departmentNames; } else { @@ -284,19 +283,17 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var lastMonthExpenses = queryable .Where(a => a.RequestDate >= oneMonthAgo && a.RequestDate <= today); - // Son 1 aydaki toplam talep miktarı - var totalRequested = lastMonthExpenses.Sum(a => a.Amount); - - // Son 1 aydaki onaylanan harcamaların toplamı - var totalApproved = lastMonthExpenses - .Where(a => a.Status == "approved") - .Sum(a => a.Amount); + // ABP 9.0.2: Separate queries for better performance + var expensesList = await AsyncExecuter.ToListAsync(lastMonthExpenses); + var totalRequested = expensesList.Sum(a => a.Amount); + var totalApproved = expensesList.Where(a => a.Status == "approved").Sum(a => a.Amount); // Son 5 kayıt - var last5Expenses = queryable - .OrderByDescending(a => a.RequestDate) - .Take(5) - .ToList(); + var last5Expenses = await AsyncExecuter.ToListAsync( + queryable + .OrderByDescending(a => a.RequestDate) + .Take(5) + ); // Map işlemleri var last5Dtos = ObjectMapper.Map, List>(last5Expenses); @@ -335,84 +332,104 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService { var today = DateTime.Now; - var employees = await _employeeRepository - .WithDetailsAsync(e => e.EmploymentType, e => e.JobPosition, e => e.Department) - .ContinueWith(t => t.Result - .Where(e => e.BirthDate.Day == today.Day && e.BirthDate.Month == today.Month) - .ToList()); + var queryable = await _employeeRepository + .WithDetailsAsync(e => e.EmploymentType, e => e.JobPosition, e => e.Department); + + var employees = await AsyncExecuter.ToListAsync( + queryable.Where(e => e.BirthDate.Day == today.Day && e.BirthDate.Month == today.Month) + ); return ObjectMapper.Map, List>(employees); } private async Task> GetUpcomingEventsAsync() { - var events = await _eventRepository - .WithDetailsAsync(e => e.Category, e => e.Type, e => e.Category, e => e.Photos, e => e.Comments) - .ContinueWith(t => t.Result.ToList().Where(e => e.isPublished).OrderByDescending(e => e.CreationTime)); + var queryable = await _eventRepository + .WithDetailsAsync(e => e.Category, e => e.Type, e => e.Photos, e => e.Comments); + + var events = await AsyncExecuter.ToListAsync( + queryable.Where(e => e.isPublished).OrderByDescending(e => e.CreationTime) + ); + + if (!events.Any()) + return new List(); + + // Tüm unique employee ID'lerini topla (event'ler ve comment'ler için) + var employeeIds = new HashSet(); + foreach (var evt in events) + { + employeeIds.Add(evt.EmployeeId); + if (evt.Comments != null) + { + foreach (var comment in evt.Comments) + { + employeeIds.Add(comment.EmployeeId); + } + } + } + + // Tüm employee'leri tek sorguda getir (N+1 problemi çözüldü) + var employeeQueryable = await _employeeRepository.WithDetailsAsync(e => e.JobPosition); + var employees = await AsyncExecuter.ToListAsync( + employeeQueryable.Where(e => employeeIds.Contains(e.Id)) + ); + var employeeDict = employees.ToDictionary(e => e.Id); var result = new List(); foreach (var evt in events) { - var employee = await _employeeRepository - .WithDetailsAsync(e => e.JobPosition) - .ContinueWith(t => t.Result.FirstOrDefault(e => e.Id == evt.EmployeeId)); + if (!employeeDict.TryGetValue(evt.EmployeeId, out var employee)) + continue; - if (employee != null) + var calendarEvent = new EventDto { - var calendarEvent = new EventDto + Id = evt.Id.ToString(), + Name = evt.Name, + Description = evt.Description, + TypeName = evt.Type?.Name, + CategoryName = evt.Category?.Name, + Date = evt.Date, + Place = evt.Place, + Organizer = new EventOrganizerDto { - Id = evt.Id.ToString(), - Name = evt.Name, - Description = evt.Description, - TypeName = evt.Type?.Name, - CategoryName = evt.Category?.Name, - Date = evt.Date, - Place = evt.Place, - Organizer = new EventOrganizerDto - { - Id = employee.Id, - Name = employee.Name, - Position = employee.JobPosition.Name, - Avatar = employee.Avatar - }, - Participants = evt.ParticipantsCount, - Photos = [], - Comments = [], - Likes = evt.Likes, - IsPublished = evt.isPublished - }; + Id = employee.Id, + Name = employee.Name, + Position = employee.JobPosition?.Name, + Avatar = employee.Avatar + }, + Participants = evt.ParticipantsCount, + Photos = [], + Comments = [], + Likes = evt.Likes, + IsPublished = evt.isPublished + }; - // Comment'lerin author bilgilerini doldur - if (evt.Comments != null && evt.Comments.Any()) + // Comment'lerin author bilgilerini doldur (artık dictionary'den hızlıca alınıyor) + if (evt.Comments != null && evt.Comments.Any()) + { + foreach (var comment in evt.Comments) { - foreach (var comment in evt.Comments) + if (employeeDict.TryGetValue(comment.EmployeeId, out var commentAuthor)) { - var commentAuthor = await _employeeRepository - .WithDetailsAsync(e => e.JobPosition) - .ContinueWith(t => t.Result.FirstOrDefault(e => e.Id == comment.EmployeeId)); - - if (commentAuthor != null) + calendarEvent.Comments.Add(new EventCommentDto { - calendarEvent.Comments.Add(new EventCommentDto + Id = comment.Id.ToString(), + Employee = new EventOrganizerDto { - Id = comment.Id.ToString(), - Employee = new EventOrganizerDto - { - Id = commentAuthor.Id, - Name = commentAuthor.Name, - Position = commentAuthor.JobPosition.Name, - Avatar = commentAuthor.Avatar - }, - Content = comment.Content, - CreationTime = comment.CreationTime, - Likes = comment.Likes - }); - } + Id = commentAuthor.Id, + Name = commentAuthor.Name, + Position = commentAuthor.JobPosition?.Name, + Avatar = commentAuthor.Avatar + }, + Content = comment.Content, + CreationTime = comment.CreationTime, + Likes = comment.Likes + }); } } - - result.Add(calendarEvent); } + + result.Add(calendarEvent); } return result; diff --git a/api/src/Erp.Platform.Application/Menu/MenuAppService.cs b/api/src/Erp.Platform.Application/Menu/MenuAppService.cs index 2b0cb81e..3b5c2502 100644 --- a/api/src/Erp.Platform.Application/Menu/MenuAppService.cs +++ b/api/src/Erp.Platform.Application/Menu/MenuAppService.cs @@ -88,29 +88,37 @@ public class MenuAppService : CrudAppService< query = ApplyPaging(query, input); var items = await AsyncExecuter.ToListAsync(query); - foreach (var item in items) + + // Tüm unique permission'ları topla + var uniquePermissions = items + .Where(x => !string.IsNullOrWhiteSpace(x.RequiredPermissionName)) + .Select(x => x.RequiredPermissionName) + .Distinct() + .ToList(); + + // Tüm permission'ları bir kerede kontrol et (N+1 query problemini önler) + var grantedPermissions = new HashSet(); + foreach (var permission in uniquePermissions) { - if (item.RequiredPermissionName.IsNullOrWhiteSpace()) + try { - entities.Add(item); + if (await AuthorizationService.IsGrantedAsync(permission)) + { + grantedPermissions.Add(permission); + } } - else + catch (Exception ex) { - try - { - var result = await AuthorizationService.IsGrantedAsync(item.RequiredPermissionName); - if (result == true) - { - entities.Add(item); - } - } - catch (Exception ex) - { - Logger.LogError(ex, ex.Message); - } + Logger.LogError(ex, $"Permission error for {permission}: {ex.Message}"); } } + // Sadece yetkili menüleri filtrele + entities = items + .Where(item => string.IsNullOrWhiteSpace(item.RequiredPermissionName) || + grantedPermissions.Contains(item.RequiredPermissionName)) + .ToList(); + entityDtos = await base.MapToGetListOutputDtosAsync(entities); } @@ -164,28 +172,44 @@ public class MenuAppService : CrudAppService< { await CheckUpdatePolicyAsync(); - var result = new List(); + if (!inputs.Any()) + return new List(); + // Id kontrolü + if (inputs.Any(x => x.Id == Guid.Empty)) + { + throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır."); + } + + // Tüm DisplayName'leri topla + var displayNames = inputs.Select(x => x.DisplayName).Distinct().ToList(); + + // Mevcut key'leri tek sorguda getir + var existingKeys = await AsyncExecuter.ToListAsync( + (await _repositoryKey.GetQueryableAsync()) + .Where(a => displayNames.Contains(a.Key) && a.ResourceName == PlatformConsts.AppName)); + + var existingKeyNames = existingKeys.Select(x => x.Key).ToHashSet(); + + // Eksik key'leri toplu ekle + var newKeys = displayNames + .Where(name => !existingKeyNames.Contains(name)) + .Select(name => new LanguageKey + { + Key = name, + ResourceName = PlatformConsts.AppName + }) + .ToList(); + + if (newKeys.Any()) + { + await _repositoryKey.InsertManyAsync(newKeys, autoSave: true); + } + + // Menüleri güncelle + var result = new List(); foreach (var input in inputs) { - if (input.Id == Guid.Empty) - { - throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır."); - } - - var key = await _repositoryKey.FirstOrDefaultAsync( - a => a.Key == input.DisplayName && - a.ResourceName == PlatformConsts.AppName); - - if (key is null) - { - await _repositoryKey.InsertAsync(new LanguageKey - { - Key = input.DisplayName, - ResourceName = PlatformConsts.AppName - }); - } - var updated = await base.UpdateAsync(input.Id, input); result.Add(updated); } @@ -221,5 +245,3 @@ public class MenuAppService : CrudAppService< return entityDtos; } } - - diff --git a/ui/src/views/list/List.tsx b/ui/src/views/list/List.tsx index e3d4e682..07cda4d8 100644 --- a/ui/src/views/list/List.tsx +++ b/ui/src/views/list/List.tsx @@ -122,14 +122,10 @@ const List: React.FC = () => {

{translate('::' + gridDto.gridOptions.title)}

-

- {translate('::' + gridDto.gridOptions.description)} -

- {/* ======================= VIEW BUTTONS ======================= */} -
+
{gridDto?.gridOptions?.layoutDto.scheduler && gridDto?.gridOptions?.schedulerOptionDto?.textExpr && gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && (