using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Logging; using Sozsoft.Languages; using Sozsoft.Languages.Entities; using Sozsoft.Languages.Languages; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Extensions; using Sozsoft.Platform.ListForms; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Volo.Abp.PermissionManagement; using Volo.Abp.TenantManagement; using static Sozsoft.Platform.Data.Seeds.SeedConsts; namespace Sozsoft.Platform.Menus; [Authorize] public class MenuAppService : CrudAppService< Menu, MenuDto, Guid, PagedAndSortedResultRequestDto> { private readonly IRepository _menuRepository; private readonly IRepository _repositoryKey; private readonly IRepository _repositoryText; private readonly ITenantRepository _tenantRepository; private readonly IPermissionDefinitionRecordRepository _permissionRepository; private readonly LanguageTextAppService _languageTextAppService; private readonly IRepository _moduleRepository; private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer; public MenuAppService( IRepository menuRepository, IRepository languageKeyRepository, IRepository languageTextRepository, ITenantRepository tenantRepository, IPermissionDefinitionRecordRepository permissionRepository, LanguageTextAppService languageTextAppService, IRepository moduleRepository, IListFormSeedDataSynchronizer seedDataSynchronizer ) : base(menuRepository) { _menuRepository = menuRepository; _repositoryKey = languageKeyRepository; _repositoryText = languageTextRepository; _tenantRepository = tenantRepository; _permissionRepository = permissionRepository; _languageTextAppService = languageTextAppService; _moduleRepository = moduleRepository; _seedDataSynchronizer = seedDataSynchronizer; CreatePolicyName = $"{AppCodes.Menus.Menu}.Create"; UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update"; DeletePolicyName = $"{AppCodes.Menus.Menu}.Delete"; } public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) { await CheckGetListPolicyAsync(); var query = await CreateFilteredQueryAsync(input); query = query.Where(a => !a.IsDisabled); //Tenant üzerinden modül bilgisi alınıp yalnızca o modüllerin menüleri listelenecek if (CurrentTenant.Id is Guid tenantId) { var tenant = await _tenantRepository.FindAsync(tenantId); if (tenant != null) { var tenantModules = tenant.GetModules(); if (tenantModules.Length > 0) { var permissionRecords = await _permissionRepository.GetListAsync(); var allowedPermissionNames = permissionRecords .Where(p => p.GetModules().Intersect(tenantModules).Any()) .Select(p => p.Name) .Distinct() .ToList(); query = query.Where(menu => string.IsNullOrEmpty(menu.RequiredPermissionName) || allowedPermissionNames.Contains(menu.RequiredPermissionName)); } } } var totalCount = await AsyncExecuter.CountAsync(query); var entities = new List(); var entityDtos = new List(); if (totalCount > 0) { query = ApplySorting(query, input); query = ApplyPaging(query, input); var items = await AsyncExecuter.ToListAsync(query); // 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) { try { if (await AuthorizationService.IsGrantedAsync(permission)) { grantedPermissions.Add(permission); } } catch (Exception ex) { 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); var languageKeys = entityDtos .Select(item => string.IsNullOrWhiteSpace(item.DisplayName) ? item.Code : item.DisplayName) .OfType() .Where(key => !string.IsNullOrWhiteSpace(key)) .Distinct() .ToList(); var languageTexts = await _repositoryText.GetListAsync(text => text.ResourceName == PlatformConsts.AppName && text.Key != null && languageKeys.Contains(text.Key) && (text.CultureName == PlatformConsts.DefaultLanguage || text.CultureName == LanguageCodes.Tr)); foreach (var menu in entityDtos) { var languageKey = string.IsNullOrWhiteSpace(menu.DisplayName) ? menu.Code : menu.DisplayName; menu.MenuTextEn = languageTexts.FirstOrDefault(text => text.Key == languageKey && text.CultureName == PlatformConsts.DefaultLanguage)?.Value; menu.MenuTextTr = languageTexts.FirstOrDefault(text => text.Key == languageKey && text.CultureName == LanguageCodes.Tr)?.Value; } } return new PagedResultDto( totalCount, entityDtos ); } public override async Task CreateAsync(MenuDto input) { await CheckCreatePolicyAsync(); await EnsureModuleAsync(input.ModuleId); var keyExists = await _repositoryKey.AnyAsync( a => a.Key == input.DisplayName && a.ResourceName == PlatformConsts.AppName); if (!keyExists) { await _repositoryKey.InsertAsync(new LanguageKey { Key = input.DisplayName, ResourceName = PlatformConsts.AppName }); } var result = await base.CreateAsync(input); await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert); return result; } public override async Task UpdateAsync(Guid id, MenuDto input) { await CheckUpdatePolicyAsync(); await EnsureModuleAsync(input.ModuleId); var key = await _repositoryKey.FirstOrDefaultAsync( a => a.Key == input.DisplayName && a.ResourceName == PlatformConsts.AppName); if (key is null) { // Yeni Key'i olustur await _repositoryKey.InsertAsync(new LanguageKey { Key = input.DisplayName, ResourceName = PlatformConsts.AppName }); } var previousCode = (await _menuRepository.GetAsync(id)).Code; var result = await base.UpdateAsync(id, input); await SyncUpdatedMenuRowsAsync([(previousCode, result.Code)]); return result; } public async Task> UpdateAllAsync(List inputs) { await CheckUpdatePolicyAsync(); 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); } var ids = inputs.Select(x => x.Id).ToList(); var previousCodes = (await _menuRepository.GetListAsync(a => ids.Contains(a.Id))) .ToDictionary(a => a.Id, a => a.Code); // Menüleri güncelle var result = new List(); foreach (var input in inputs) { var updated = await base.UpdateAsync(input.Id, input); result.Add(updated); } await SyncUpdatedMenuRowsAsync(result.Select(x => (previousCodes.GetValueOrDefault(x.Id), x.Code))); return result; } public override async Task DeleteAsync(Guid id) { await CheckDeletePolicyAsync(); var menu = await _menuRepository.GetAsync(id); if (menu != null) { await _repositoryKey.DeleteAsync( a => a.Key == menu.DisplayName && a.ResourceName == PlatformConsts.AppName); } await base.DeleteAsync(id); if (menu != null) await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, [menu.Code]); } public async Task> GetListMainMenuAsync() { await CheckGetPolicyAsync(); var query = await _menuRepository.GetQueryableAsync(); // Public site menusu yonetim kenar cubugunda yer almaz; o agac ziyaretci // duzenine aittir ve uygulama konfigurasyonu uzerinden tasinir. query = query.Where(a => !a.IsDisabled && a.ModuleId != null && a.ModuleId != PlatformConsts.PublicSite.ModuleId); var entities = await AsyncExecuter.ToListAsync(query); var entityDtos = await MapToGetListOutputDtosAsync(entities); return entityDtos; } /// /// Menüyü tüm alanlarıyla günceller ve görünen adın dil metinlerini de yazar. /// Kod (Code) dil anahtarı ve yetki eşleşmesi olduğu için değiştirilemez; gönderilen /// değer yok sayılıp mevcut kod korunur. /// public async Task UpdateWithLanguageKeyTextAsync(Guid id, MenuDto input) { await CheckUpdatePolicyAsync(); await EnsureModuleAsync(input.ModuleId); var menu = await _menuRepository.GetAsync(id); // Kod değiştirilemez: dil anahtarı, alt menülerin ParentCode'u ve permission // kayıtları bu değere bağlıdır. input.Code = menu.Code; await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr); var result = await base.UpdateAsync(id, input); await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Update); return result; } /// /// Menü koduna karşılık gelen dil anahtarını ve EN/TR metinlerini oluşturur veya günceller. /// private async Task UpsertMenuLanguageTextsAsync(string code, string menuTextEn, string menuTextTr) { var keyExists = await _repositoryKey.AnyAsync( a => a.Key == code && a.ResourceName == PlatformConsts.AppName); if (!keyExists) { await _repositoryKey.InsertAsync(new LanguageKey { Key = code, ResourceName = PlatformConsts.AppName }, autoSave: true); } var changedTextIds = new List(); var insertedTextIds = new List(); foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) }) { if (value == null) continue; var existing = await _repositoryText.FirstOrDefaultAsync( a => a.CultureName == culture && a.Key == code && a.ResourceName == PlatformConsts.AppName); if (existing != null) { existing.Value = value; await _repositoryText.UpdateAsync(existing, autoSave: true); changedTextIds.Add(existing.Id); } else { var inserted = await _repositoryText.InsertAsync(new LanguageText { Key = code, CultureName = culture, Value = value, ResourceName = PlatformConsts.AppName }, autoSave: true); insertedTextIds.Add(inserted.Id); } } await _languageTextAppService.ClearRedisCacheAsync(); // Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da // degistirmek DB sifirlandiginda geri gelmeli. await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, changedTextIds, SeedSyncOperation.Update); await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, insertedTextIds, SeedSyncOperation.Insert); } public async Task CreateWithLanguageKeyTextAsync(MenuDto input) { await CheckCreatePolicyAsync(); await EnsureModuleAsync(input.ModuleId); await UpsertMenuLanguageTextsAsync(input.Code, input.MenuTextEn, input.MenuTextTr); var result = await base.CreateAsync(input); await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert); return result; } /// /// Menu Yoneticisi de App.Menus.Menu ekraniyla ayni seed dosyasini yazar; aksi halde /// siralama ve duzenlemeler DB sifirlandiginda kaybolur. Dosyanin anahtari Code'dur. /// private Task SyncMenuRowsAsync(IReadOnlyCollection codes, SeedSyncOperation operation) => _seedDataSynchronizer.SyncRowsAsync(AppCodes.Menus.Menu, [.. codes], operation); /// /// Kodu degisen menunun eski satiri dosyadan cikarilir; dosya Code anahtarli oldugu icin /// yeni kod ayri bir satir olarak eklenir. /// private async Task SyncUpdatedMenuRowsAsync(IEnumerable<(string? PreviousCode, string Code)> changes) { var list = changes.ToList(); var renamed = list .Where(x => x.PreviousCode != x.Code) .Select(x => x.PreviousCode) .OfType() .Where(code => !string.IsNullOrWhiteSpace(code)) .Cast() .ToArray(); if (renamed.Length > 0) await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, renamed); await SyncMenuRowsAsync([.. list.Select(x => x.Code)], SeedSyncOperation.Update); } /// /// Menude secilen modul tanimli degilse olusturur; kok menude girilen kisa kod /// dogrudan yeni bir modul olur ve ModuleId yabanci anahtari bos kalmaz. /// private async Task EnsureModuleAsync(string moduleId) { var cleaned = moduleId?.Trim(); if (string.IsNullOrWhiteSpace(cleaned)) { return; } if (await _moduleRepository.AnyAsync(a => a.Id == cleaned)) { return; } await _moduleRepository.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: true); } public async Task> GetModulesAsync() { var modules = await _moduleRepository.GetListAsync(); return modules .OrderBy(module => module.Name) .Select(module => new LookupDataDto { Key = module.Id, Name = module.Name }) .ToList(); } }