using System; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Dashboard; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.Dashboard; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // DashboardPromotionAdapter — IPromotableEntity for MDASHBOARD ("DASHBOARD"). // Parent row only in this first pass — the Dashboard's Page placement // (MDASHBOARDPAGE: tab order/default-tab/visibility) is a real, portable // structural child (its own DashboardPageDTO already carries PageCode, // resolvable via PagePromotionAdapter's IPageCodeLookupDAL once that's // registered) but is deliberately NOT bundled yet — a follow-up, not a // silent gap, same "parent first, children later" pattern already used // for ReportView's own Fields. // // OwnerId/ImageId/DefaultCriteriaConfigId all default to -1 at the DB // level with NO foreign key constraint on any of them (confirmed via // schema query) — unlike ReportView's TemplateId/PageId, forcing these to // -1 at the destination is completely safe, not just a workaround. // OwnerId specifically: DashboardBLL.SaveDashboard already reassigns a // Personal (DashboardType=0) dashboard's OwnerId to the importing user // when left at its default, so no extra handling is needed here for that // case; a Shared/Role/System dashboard's own OwnerId is simply never // carried across environments either, since a user ID from the origin // means nothing at the destination. // // ModuleId/ModuleCode reuses the existing IModuleCodeLookupDAL — module // codes are static/shared across installs, same reasoning as // Page/ReportView/DrillDown's own Module references. // ============================================================ public class DashboardPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "DASHBOARD"; private readonly IDashboardBLL _DashboardBLL; private readonly IModuleCodeLookupDAL _ModuleCodeLookup; private readonly IDashboardCodeLookupDAL _DashboardCodeLookup; public DashboardPromotionAdapter( IDashboardBLL dashboardBLL, IModuleCodeLookupDAL moduleCodeLookup, IDashboardCodeLookupDAL dashboardCodeLookup) { _DashboardBLL = dashboardBLL; _ModuleCodeLookup = moduleCodeLookup; _DashboardCodeLookup = dashboardCodeLookup; } private class PortablePayload { public string DashboardCode { get; set; } = string.Empty; public string DashboardName { get; set; } = string.Empty; public byte DashboardType { get; set; } public string? ModuleCode { get; set; } public string? ImageLinkURL { get; set; } public string? Remarks { get; set; } public short SortOrder { get; set; } } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _DashboardBLL.GetDashboard(originEntityId, login, ct).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"Dashboard {originEntityId} not found."); string? moduleCode = null; if (dto.ModuleId != 0 && dto.ModuleId != -1) { moduleCode = await _ModuleCodeLookup.GetModuleCodeById(dto.ModuleId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Dashboard {originEntityId} references ModuleId {dto.ModuleId}, which has no ModuleCode — cannot promote."); } var payload = new PortablePayload { DashboardCode = dto.DashboardCode, DashboardName = dto.DashboardName, DashboardType = dto.DashboardType, ModuleCode = moduleCode, ImageLinkURL = dto.ImageLinkURL, Remarks = dto.Remarks, SortOrder = dto.SortOrder }; return new PromotionBundleDTO { EntityTypeCode = EntityTypeCode, OriginEntityId = originEntityId, PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload) }; } public Task ValidateForPromotionAsync( PromotionBundleDTO bundle, CancellationToken ct) { var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson); var result = payload is null ? PromotionValidationResultDTO.Block("Dashboard payload could not be parsed.") : payload.DashboardType > 3 ? PromotionValidationResultDTO.Block("Dashboard.DashboardType is out of the valid range (0-3).") : PromotionValidationResultDTO.Ok(); return Task.FromResult(result); } public async Task ImportPromotedAsync( PromotionBundleDTO bundle, byte destinationSourceType, int? existingDestinationId, LoginDTO login, CancellationToken ct) { var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson) ?? throw new InvalidOperationException("Dashboard promotion payload could not be parsed."); var moduleId = -1; if (!string.IsNullOrWhiteSpace(payload.ModuleCode)) { moduleId = await _ModuleCodeLookup.GetModuleIdByCode(payload.ModuleCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Module with ModuleCode '{payload.ModuleCode}' exists at this destination."); } // If this is an update to a previously-promoted dashboard, existingDestinationId // already identifies it directly. Otherwise, check whether a dashboard with this code // already exists at the destination (created independently, not via a prior promotion) // so Import updates it rather than violating DashboardCode's own unique constraint with // a duplicate insert — DASHBOARDCODE has a real UNIQUE constraint (UK_MDASHBOARD_ // DASHBOARDCODE, confirmed via schema), the same class of collision already hit live // with ReportView's own ReportViewName. var resolvedId = existingDestinationId ?? await _DashboardCodeLookup.GetDashboardIdByCode(payload.DashboardCode, login, ct).ConfigureAwait(false) ?? 0; var dto = new DashboardDTO { DashboardId = resolvedId, DashboardCode = payload.DashboardCode, DashboardName = payload.DashboardName, DashboardType = payload.DashboardType, ModuleId = moduleId, OwnerId = -1, // never mapped — see class header comment ImageId = -1, // never mapped — see class header comment ImageLinkURL = payload.ImageLinkURL, Remarks = payload.Remarks, DefaultCriteriaConfigId = -1, // never mapped — see class header comment SortOrder = payload.SortOrder, SourceType = destinationSourceType }; await _DashboardBLL.SaveDashboard(dto, login, ct).ConfigureAwait(false); return dto.DashboardId; // mutated in place by SaveDashboard when isNew } } }