using CMSDAL.CustomCode.Navigation; using CMSDAL.DTO.Navigation; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using GB5Shared.Validation; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; namespace CMSBLL.Navigation; public class NavigationItemBLL : INavigationItemBLL { private readonly INavigationItemDAL _dal; private readonly INavigationDAL _navigationDal; private readonly ICrossTenantGuard _guard; public NavigationItemBLL(INavigationItemDAL dal, INavigationDAL navigationDal, ICrossTenantGuard guard) { _dal = dal; _navigationDal = navigationDal; _guard = guard; } public async Task> GetNavigationTreeAsync(int navigationId, LoginDTO loginDTO, CancellationToken ct = default) { try { var flat = await _dal.GetFlatByNavigationAsync(navigationId, loginDTO, ct); var nodesById = flat.ToDictionary(f => f.NavigationItemId, f => new NavigationItemNodeDTO { NavigationItemId = f.NavigationItemId, Label = f.Label, LinkType = f.LinkType, ResolvedUrl = f.LinkType == NavigationLinkType.InternalPage ? f.SitePageUrlPath : f.ExternalUrl, IsNewTab = f.IsNewTab }); var roots = new List(); foreach (var flatItem in flat) { var node = nodesById[flatItem.NavigationItemId]; if (flatItem.ParentNavigationItemId.HasValue && nodesById.TryGetValue(flatItem.ParentNavigationItemId.Value, out var parent)) parent.Children.Add(node); else roots.Add(node); } return roots; } catch { throw; } } public async Task SaveItem(NavigationItemDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { // Captured before the save — SaveItem mutates dto.NavigationItemId to the new identity // as a side effect, so this must not be re-derived from dto afterward. var isNew = dto.NavigationItemId == 0; try { switch (dto.LinkType) { case NavigationLinkType.InternalPage when dto.SitePageId is null or 0: throw new ValidationException("SitePageId is required when LinkType is InternalPage."); case NavigationLinkType.ExternalUrl or NavigationLinkType.Anchor when string.IsNullOrWhiteSpace(dto.ExternalUrl): throw new ValidationException("ExternalUrl is required when LinkType is ExternalURL or Anchor."); } // Only a brand-new item attaches to a caller-supplied NavigationId — without this // check, any tenant could attach an item to another tenant's Navigation by guessing // its NavigationId. if (isNew) { var ownerTenantId = await _navigationDal.GetNavigationTenantIdAsync(dto.NavigationId, loginDTO, ct); _guard.EnsureOwnedByTenant(ownerTenantId, loginDTO.ClientId, "Navigation", dto.NavigationId); } dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; if (isNew) { dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; } var itemId = isNew ? await _dal.SaveItem(dto, loginDTO, ct: ct) : await _dal.UpdateItem(dto, loginDTO, ct: ct); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {itemId}" : SuccessResponse.UpdateSuccess; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch { throw; } } public async Task DeleteItem(int navigationItemId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.DeleteItem(navigationItemId, loginDTO, ct); } catch { throw; } } }