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(
.OrderByDescending(s => s.StartDate) queryable
.Take(3) .Where(s => s.Priority == "High" || s.Priority == "Urgent")
.ToList()); .OrderByDescending(s => s.StartDate)
.Take(3)
);
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(
.Where(s => s.Status == "active") queryable
.Include(s => s.Questions) .Where(s => s.Status == "active")
.ThenInclude(q => q.Options) .Include(s => s.Questions)
.ToList(); .ThenInclude(q => q.Options)
);
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(
.OrderByDescending(a => a.RequestDate) queryable
.Take(5) .OrderByDescending(a => a.RequestDate)
.ToList(); .Take(5)
);
// 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,84 +332,104 @@ 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(),
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(), Id = employee.Id,
Name = evt.Name, Name = employee.Name,
Description = evt.Description, Position = employee.JobPosition?.Name,
TypeName = evt.Type?.Name, Avatar = employee.Avatar
CategoryName = evt.Category?.Name, },
Date = evt.Date, Participants = evt.ParticipantsCount,
Place = evt.Place, Photos = [],
Organizer = new EventOrganizerDto Comments = [],
{ Likes = evt.Likes,
Id = employee.Id, IsPublished = evt.isPublished
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 // 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) if (employeeDict.TryGetValue(comment.EmployeeId, out var commentAuthor))
{ {
var commentAuthor = await _employeeRepository calendarEvent.Comments.Add(new EventCommentDto
.WithDetailsAsync(e => e.JobPosition)
.ContinueWith(t => t.Result.FirstOrDefault(e => e.Id == comment.EmployeeId));
if (commentAuthor != null)
{ {
calendarEvent.Comments.Add(new EventCommentDto Id = comment.Id.ToString(),
Employee = new EventOrganizerDto
{ {
Id = comment.Id.ToString(), Id = commentAuthor.Id,
Employee = new EventOrganizerDto Name = commentAuthor.Name,
{ Position = commentAuthor.JobPosition?.Name,
Id = commentAuthor.Id, Avatar = commentAuthor.Avatar
Name = commentAuthor.Name, },
Position = commentAuthor.JobPosition.Name, Content = comment.Content,
Avatar = commentAuthor.Avatar CreationTime = comment.CreationTime,
}, Likes = comment.Likes
Content = comment.Content, });
CreationTime = comment.CreationTime,
Likes = comment.Likes
});
}
} }
} }
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
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<string>();
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 Logger.LogError(ex, $"Permission error for {permission}: {ex.Message}");
{
var result = await AuthorizationService.IsGrantedAsync(item.RequiredPermissionName);
if (result == true)
{
entities.Add(item);
}
}
catch (Exception ex)
{
Logger.LogError(ex, 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>();
// 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<MenuDto>();
foreach (var input in inputs) 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); 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 && (