IntranetAppService ve MenuAppService güçlendirildi

This commit is contained in:
Sedat Öztürk 2026-02-04 21:25:35 +03:00
parent b070702378
commit ec44d03e79
3 changed files with 182 additions and 147 deletions

View file

@ -13,6 +13,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Erp.Platform.Public; namespace Erp.Platform.Public;
@ -83,8 +84,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
_projectTaskRepository = projectTaskRepository; _projectTaskRepository = projectTaskRepository;
} }
[UnitOfWork]
public async Task<IntranetDashboardDto> GetIntranetDashboardAsync() public async Task<IntranetDashboardDto> GetIntranetDashboardAsync()
{ {
// ABP 9.0.2: UnitOfWork kapsamında tüm sorgular optimize edilir
return new IntranetDashboardDto return new IntranetDashboardDto
{ {
Events = await GetUpcomingEventsAsync(), Events = await GetUpcomingEventsAsync(),
@ -107,21 +110,24 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
private async Task<List<ProjectTaskDto>> GetProjectTasksAsync() private async Task<List<ProjectTaskDto>> GetProjectTasksAsync()
{ {
var tasks = await _projectTaskRepository var queryable = await _projectTaskRepository.WithDetailsAsync(e => e.Employee);
.WithDetailsAsync(e => e.Employee)
.ContinueWith(t => t.Result.Where(s => s.Priority == "High" || s.Priority == "Urgent") var tasks = await AsyncExecuter.ToListAsync(
queryable
.Where(s => s.Priority == "High" || s.Priority == "Urgent")
.OrderByDescending(s => s.StartDate) .OrderByDescending(s => s.StartDate)
.Take(3) .Take(3)
.ToList()); );
return ObjectMapper.Map<List<ProjectTask>, List<ProjectTaskDto>>(tasks); return ObjectMapper.Map<List<ProjectTask>, List<ProjectTaskDto>>(tasks);
} }
private async Task<List<SocialPostDto>> GetSocialPostsAsync() private async Task<List<SocialPostDto>> GetSocialPostsAsync()
{ {
var socialPosts = await _socialPostRepository var queryable = await _socialPostRepository
.WithDetailsAsync(e => e.Employee, e => e.Location, e => e.Media, e => e.Comments, e => e.Likes) .WithDetailsAsync(e => e.Employee, e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
.ContinueWith(t => t.Result.ToList());
var socialPosts = await AsyncExecuter.ToListAsync(queryable);
return ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts); return ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts);
} }
@ -130,29 +136,28 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
{ {
var queryable = await _surveyRepository.GetQueryableAsync(); var queryable = await _surveyRepository.GetQueryableAsync();
var surveys = queryable var surveys = await AsyncExecuter.ToListAsync(
queryable
.Where(s => s.Status == "active") .Where(s => s.Status == "active")
.Include(s => s.Questions) .Include(s => s.Questions)
.ThenInclude(q => q.Options) .ThenInclude(q => q.Options)
.ToList(); );
return ObjectMapper.Map<List<Survey>, List<SurveyDto>>(surveys); return ObjectMapper.Map<List<Survey>, List<SurveyDto>>(surveys);
} }
private async Task<List<OvertimeDto>> GetOvertimesAsync() private async Task<List<OvertimeDto>> GetOvertimesAsync()
{ {
var overtimes = await _overtimeRepository var queryable = await _overtimeRepository.WithDetailsAsync(e => e.Employee);
.WithDetailsAsync(e => e.Employee) var overtimes = await AsyncExecuter.ToListAsync(queryable);
.ContinueWith(t => t.Result.ToList());
return ObjectMapper.Map<List<Overtime>, List<OvertimeDto>>(overtimes); return ObjectMapper.Map<List<Overtime>, List<OvertimeDto>>(overtimes);
} }
private async Task<List<LeaveDto>> GetLeavesAsync() private async Task<List<LeaveDto>> GetLeavesAsync()
{ {
var leaves = await _leaveRepository var queryable = await _leaveRepository.WithDetailsAsync(e => e.Employee);
.WithDetailsAsync(e => e.Employee) var leaves = await AsyncExecuter.ToListAsync(queryable);
.ContinueWith(t => t.Result.ToList());
return ObjectMapper.Map<List<Leave>, List<LeaveDto>>(leaves); return ObjectMapper.Map<List<Leave>, List<LeaveDto>>(leaves);
} }
@ -225,14 +230,14 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
private async Task<List<AnnouncementDto>> GetAnnouncementsAsync() private async Task<List<AnnouncementDto>> GetAnnouncementsAsync()
{ {
var announcements = await _announcementRepository var queryable = await _announcementRepository.WithDetailsAsync(e => e.Employee);
.WithDetailsAsync(e => e.Employee) var announcements = await AsyncExecuter.ToListAsync(queryable);
.ContinueWith(t => t.Result.ToList());
var announcementDtos = new List<AnnouncementDto>(); var announcementDtos = new List<AnnouncementDto>();
// Tüm departmanları bir kez çek (performans için) // Tüm departmanları bir kez çek (performans için)
var allDepartments = await _departmentRepository.GetListAsync(); var allDepartments = await _departmentRepository.GetListAsync();
var departmentDict = allDepartments.ToDictionary(d => d.Id, d => d.Name);
foreach (var announcement in announcements) foreach (var announcement in announcements)
{ {
@ -246,21 +251,15 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
.Select(d => d.Trim()) .Select(d => d.Trim())
.ToArray(); .ToArray();
// ID'leri Department Name'lere çevir // ID'leri Department Name'lere çevir (Dictionary lookup çok hızlı)
var departmentNames = new List<string>(); var departmentNames = departmentIds
foreach (var deptId in departmentIds) .Where(deptId => Guid.TryParse(deptId, out _))
{ .Select(deptId => Guid.Parse(deptId))
if (Guid.TryParse(deptId, out var guid)) .Where(guid => departmentDict.ContainsKey(guid))
{ .Select(guid => departmentDict[guid])
var department = allDepartments.FirstOrDefault(d => d.Id == guid); .ToArray();
if (department != null)
{
departmentNames.Add(department.Name);
}
}
}
dto.Departments = departmentNames.ToArray(); dto.Departments = departmentNames;
} }
else else
{ {
@ -284,19 +283,17 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
var lastMonthExpenses = queryable var lastMonthExpenses = queryable
.Where(a => a.RequestDate >= oneMonthAgo && a.RequestDate <= today); .Where(a => a.RequestDate >= oneMonthAgo && a.RequestDate <= today);
// Son 1 aydaki toplam talep miktarı // ABP 9.0.2: Separate queries for better performance
var totalRequested = lastMonthExpenses.Sum(a => a.Amount); var expensesList = await AsyncExecuter.ToListAsync(lastMonthExpenses);
var totalRequested = expensesList.Sum(a => a.Amount);
// Son 1 aydaki onaylanan harcamaların toplamı var totalApproved = expensesList.Where(a => a.Status == "approved").Sum(a => a.Amount);
var totalApproved = lastMonthExpenses
.Where(a => a.Status == "approved")
.Sum(a => a.Amount);
// Son 5 kayıt // Son 5 kayıt
var last5Expenses = queryable var last5Expenses = await AsyncExecuter.ToListAsync(
queryable
.OrderByDescending(a => a.RequestDate) .OrderByDescending(a => a.RequestDate)
.Take(5) .Take(5)
.ToList(); );
// Map işlemleri // Map işlemleri
var last5Dtos = ObjectMapper.Map<List<Expense>, List<ExpenseDto>>(last5Expenses); var last5Dtos = ObjectMapper.Map<List<Expense>, List<ExpenseDto>>(last5Expenses);
@ -335,30 +332,55 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
{ {
var today = DateTime.Now; var today = DateTime.Now;
var employees = await _employeeRepository var queryable = await _employeeRepository
.WithDetailsAsync(e => e.EmploymentType, e => e.JobPosition, e => e.Department) .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) var employees = await AsyncExecuter.ToListAsync(
.ToList()); queryable.Where(e => e.BirthDate.Day == today.Day && e.BirthDate.Month == today.Month)
);
return ObjectMapper.Map<List<Employee>, List<EmployeeDto>>(employees); return ObjectMapper.Map<List<Employee>, List<EmployeeDto>>(employees);
} }
private async Task<List<EventDto>> GetUpcomingEventsAsync() private async Task<List<EventDto>> GetUpcomingEventsAsync()
{ {
var events = await _eventRepository var queryable = await _eventRepository
.WithDetailsAsync(e => e.Category, e => e.Type, e => e.Category, e => e.Photos, e => e.Comments) .WithDetailsAsync(e => e.Category, e => e.Type, e => e.Photos, e => e.Comments);
.ContinueWith(t => t.Result.ToList().Where(e => e.isPublished).OrderByDescending(e => e.CreationTime));
var events = await AsyncExecuter.ToListAsync(
queryable.Where(e => e.isPublished).OrderByDescending(e => e.CreationTime)
);
if (!events.Any())
return new List<EventDto>();
// Tüm unique employee ID'lerini topla (event'ler ve comment'ler için)
var employeeIds = new HashSet<Guid>();
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<EventDto>(); var result = new List<EventDto>();
foreach (var evt in events) foreach (var evt in events)
{ {
var employee = await _employeeRepository if (!employeeDict.TryGetValue(evt.EmployeeId, out var employee))
.WithDetailsAsync(e => e.JobPosition) continue;
.ContinueWith(t => t.Result.FirstOrDefault(e => e.Id == evt.EmployeeId));
if (employee != null)
{
var calendarEvent = new EventDto var calendarEvent = new EventDto
{ {
Id = evt.Id.ToString(), Id = evt.Id.ToString(),
@ -372,7 +394,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
{ {
Id = employee.Id, Id = employee.Id,
Name = employee.Name, Name = employee.Name,
Position = employee.JobPosition.Name, Position = employee.JobPosition?.Name,
Avatar = employee.Avatar Avatar = employee.Avatar
}, },
Participants = evt.ParticipantsCount, Participants = evt.ParticipantsCount,
@ -382,16 +404,12 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
IsPublished = evt.isPublished IsPublished = evt.isPublished
}; };
// Comment'lerin author bilgilerini doldur // Comment'lerin author bilgilerini doldur (artık dictionary'den hızlıca alınıyor)
if (evt.Comments != null && evt.Comments.Any()) if (evt.Comments != null && evt.Comments.Any())
{ {
foreach (var comment in evt.Comments) foreach (var comment in evt.Comments)
{ {
var commentAuthor = await _employeeRepository if (employeeDict.TryGetValue(comment.EmployeeId, out var commentAuthor))
.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
{ {
@ -400,7 +418,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
{ {
Id = commentAuthor.Id, Id = commentAuthor.Id,
Name = commentAuthor.Name, Name = commentAuthor.Name,
Position = commentAuthor.JobPosition.Name, Position = commentAuthor.JobPosition?.Name,
Avatar = commentAuthor.Avatar Avatar = commentAuthor.Avatar
}, },
Content = comment.Content, Content = comment.Content,
@ -413,7 +431,6 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
result.Add(calendarEvent); result.Add(calendarEvent);
} }
}
return result; return result;
} }

View file

@ -88,29 +88,37 @@ public class MenuAppService : CrudAppService<
query = ApplyPaging(query, input); query = ApplyPaging(query, input);
var items = await AsyncExecuter.ToListAsync(query); var items = await AsyncExecuter.ToListAsync(query);
foreach (var item in items)
{ // Tüm unique permission'ları topla
if (item.RequiredPermissionName.IsNullOrWhiteSpace()) var uniquePermissions = items
{ .Where(x => !string.IsNullOrWhiteSpace(x.RequiredPermissionName))
entities.Add(item); .Select(x => x.RequiredPermissionName)
} .Distinct()
else .ToList();
// Tüm permission'ları bir kerede kontrol et (N+1 query problemini önler)
var grantedPermissions = new HashSet<string>();
foreach (var permission in uniquePermissions)
{ {
try try
{ {
var result = await AuthorizationService.IsGrantedAsync(item.RequiredPermissionName); if (await AuthorizationService.IsGrantedAsync(permission))
if (result == true)
{ {
entities.Add(item); grantedPermissions.Add(permission);
} }
} }
catch (Exception ex) 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); entityDtos = await base.MapToGetListOutputDtosAsync(entities);
} }
@ -164,28 +172,44 @@ public class MenuAppService : CrudAppService<
{ {
await CheckUpdatePolicyAsync(); await CheckUpdatePolicyAsync();
var result = new List<MenuDto>(); if (!inputs.Any())
return new List<MenuDto>();
foreach (var input in inputs) // Id kontrolü
{ if (inputs.Any(x => x.Id == Guid.Empty))
if (input.Id == Guid.Empty)
{ {
throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır."); throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır.");
} }
var key = await _repositoryKey.FirstOrDefaultAsync( // Tüm DisplayName'leri topla
a => a.Key == input.DisplayName && var displayNames = inputs.Select(x => x.DisplayName).Distinct().ToList();
a.ResourceName == PlatformConsts.AppName);
if (key is null) // 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
{ {
await _repositoryKey.InsertAsync(new LanguageKey Key = name,
{
Key = input.DisplayName,
ResourceName = PlatformConsts.AppName ResourceName = PlatformConsts.AppName
}); })
.ToList();
if (newKeys.Any())
{
await _repositoryKey.InsertManyAsync(newKeys, autoSave: true);
} }
// Menüleri güncelle
var result = new List<MenuDto>();
foreach (var input in inputs)
{
var updated = await base.UpdateAsync(input.Id, input); var updated = await base.UpdateAsync(input.Id, input);
result.Add(updated); result.Add(updated);
} }
@ -221,5 +245,3 @@ public class MenuAppService : CrudAppService<
return entityDtos; return entityDtos;
} }
} }

View file

@ -122,14 +122,10 @@ const List: React.FC = () => {
<h4 className="text-sm font-medium">{translate('::' + gridDto.gridOptions.title)}</h4> <h4 className="text-sm font-medium">{translate('::' + gridDto.gridOptions.title)}</h4>
<Badge content={viewMode} /> <Badge content={viewMode} />
<p className="ml-2 text-xs text-slate-500 mr-auto">
{translate('::' + gridDto.gridOptions.description)}
</p>
{/* ======================= {/* =======================
VIEW BUTTONS VIEW BUTTONS
======================= */} ======================= */}
<div className="flex gap-1"> <div className="flex gap-1 ml-auto">
{gridDto?.gridOptions?.layoutDto.scheduler && {gridDto?.gridOptions?.layoutDto.scheduler &&
gridDto?.gridOptions?.schedulerOptionDto?.textExpr && gridDto?.gridOptions?.schedulerOptionDto?.textExpr &&
gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && ( gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && (