using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Entitlement; using FrameworkDAL.CustomCode.Dashboard; using FrameworkDAL.DTO.Dashboard; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Newtonsoft.Json; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.Dashboard { public class DashboardBLL : IDashboardBLL { private readonly IDashboardDAL _DashboardDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly EventLogPublish _EventLog; private readonly IEntitlementGateClient _EntitlementGate; private readonly KeyInvalidate _KeyInvalidate; public DashboardBLL(IDashboardDAL DashboardDAL, AutoNumber AutoNumber, IValidation Validation, EventLogPublish EventLog, IEntitlementGateClient EntitlementGate, KeyInvalidate KeyInvalidate) { _DashboardDAL = DashboardDAL; _AutoNumber = AutoNumber; _Validation = Validation; _EventLog = EventLog; _EntitlementGate = EntitlementGate; _KeyInvalidate = KeyInvalidate; } // SourceType tiers per Phase 5 migration header: 1=Framework, 2=DevAdmin are // Standard (vendor-authored, eligible for Entitlement gating); 3=ImpAdmin, // 4=Admin, 5=User are Custom (tenant-authored, gating bypassed). private static bool IsStandardSourceType(byte sourceType) => sourceType == 1 || sourceType == 2; public async Task GetDashboard(int DashboardId, LoginDTO LoginDTO, CancellationToken ct = default) { try { return await _DashboardDAL.GetDashboard(DashboardId, LoginDTO, ct); } catch (Exception) { throw; } } public async Task SaveDashboard(DashboardDTO DashboardDTO, LoginDTO LoginDTO, CancellationToken ct = default) { if (DashboardDTO == null) throw new ArgumentNullException(nameof(DashboardDTO)); AutoNumberDTO? autoNumberDTO = null; try { await _Validation.NotEmpty(DashboardDTO.DashboardCode, nameof(DashboardDTO.DashboardCode)); await _Validation.NotEmpty(DashboardDTO.DashboardName, nameof(DashboardDTO.DashboardName)); if (DashboardDTO.DashboardType > 3) throw new ArgumentException("DashboardType must be 0 (Personal), 1 (Shared), 2 (Role), or 3 (System)."); if (DashboardDTO.DefaultCriteriaConfigId > 0 && !await _DashboardDAL.CriteriaConfigExists(DashboardDTO.DefaultCriteriaConfigId, LoginDTO, ct)) throw new ArgumentException($"DefaultCriteriaConfigId {DashboardDTO.DefaultCriteriaConfigId} does not refer to an active criteria config."); // Personal dashboards default to the caller as owner when not explicitly set. if (DashboardDTO.DashboardType == 0 && DashboardDTO.OwnerId <= 0) DashboardDTO.OwnerId = LoginDTO.UserId; DashboardDTO.ModifiedById = LoginDTO.UserId; DashboardDTO.ModifiedOn = DateTime.UtcNow; var isNew = DashboardDTO.DashboardId == 0; GB5Trace.Step("validate-dashboard", new { DashboardDTO.DashboardId, isNew }); if (isNew) { autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "DASHBOARD", LoginDTO); DashboardDTO.DashboardId = autoNumberDTO.StartNumber; DashboardDTO.CreatedById = LoginDTO.UserId; DashboardDTO.CreatedOn = DateTime.UtcNow; GB5Trace.Step("save-dashboard", new { DashboardDTO.DashboardId, isNew }); await _DashboardDAL.SaveDashboard(DashboardDTO, LoginDTO, ct); } else { GB5Trace.Step("save-dashboard", new { DashboardDTO.DashboardId, isNew }); await _DashboardDAL.UpdateDashboard(DashboardDTO, LoginDTO, ct); } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEDASHBOARDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( isNew ? "Dashboard Created" : "Dashboard Updated", new { DashboardDTO.DashboardId, DashboardDTO.DashboardCode, DashboardDTO.DashboardType }, EventTypeConstant.SAVEDASHBOARDEVENTTYPEID, DashboardDTO.DashboardId, LoginDTO, ct: ct).ConfigureAwait(false); // GetDashboardListForUser is cached per (UserId, RoleId) — a Dashboard save // can change visibility for an unbounded set of users (Shared/System show to // everyone; Role dashboards show to every user with that role), so there's no // single deterministic key to target the way CLIENT_LEVEL caches elsewhere in // this codebase do. Dashboard saves are rare/admin-driven, so a full cache // flush (KeyInvalidate.AllInvalidateCache("-1")) is an acceptable tradeoff over // leaving Phase 2's promised-but-never-wired invalidation gap in place (default // 300s TTL previously meant stale dashboard visibility for up to 5 minutes). await _KeyInvalidate.AllInvalidateCache("-1").ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {DashboardDTO.DashboardId}" : $"{SuccessResponse.UpdateSuccessMessage} {DashboardDTO.DashboardId}"; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { if (autoNumberDTO != null && DashboardDTO.DashboardId > 0) await _AutoNumber.RollbackAutoNumber("DASHBOARD", DashboardDTO.DashboardId, LoginDTO); GB5Trace.MarkFailed("save-dashboard-failed", ex); throw; } } public async Task DeleteDashboard(int DashboardId, LoginDTO LoginDTO, CancellationToken ct = default) { try { GB5Trace.Step("delete-dashboard", new { DashboardId }); var result = await _DashboardDAL.DeleteDashboard(DashboardId, LoginDTO, ct); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETEDASHBOARDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "Dashboard Deleted", new { DashboardId }, EventTypeConstant.DELETEDASHBOARDEVENTTYPEID, DashboardId, LoginDTO, ct: ct).ConfigureAwait(false); await _KeyInvalidate.AllInvalidateCache("-1").ConfigureAwait(false); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-dashboard-failed", ex); throw; } } public async Task GetSelectListDashboard(CriteriaDTO CriteriaDTO, LoginDTO LoginDTO, CancellationToken ct = default) { try { return await _DashboardDAL.GetSelectListDashboard(CriteriaDTO, LoginDTO, ct); } catch (Exception) { throw; } } public async Task GetDashboardListForUser(int UserId, int RoleId, LoginDTO LoginDTO, CancellationToken ct = default) { try { var dashboards = await _DashboardDAL.GetDashboardListForUser(UserId, RoleId, LoginDTO, ct); var visible = new List(dashboards.Count); foreach (var dashboard in dashboards) { if (IsStandardSourceType(dashboard.SourceType) && !string.IsNullOrWhiteSpace(dashboard.FeatureCode)) { var enabled = await _EntitlementGate.IsFeatureEnabledAsync(LoginDTO.ClientId, UserId, dashboard.FeatureCode, ct); if (!enabled) continue; } visible.Add(dashboard); } return JsonConvert.SerializeObject(visible); } catch (Exception) { throw; } } } }