diff --git a/e2e/screenshots.spec.ts b/e2e/screenshots.spec.ts index e06578a..e03e6ec 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -391,18 +391,41 @@ const { screenlyJsContent: reportScreenlyJsContent } = } ) +const { screenlyJsContent: dashboardLabelsScreenlyJsContent } = + createMockScreenlyForScreenshots( + { coordinates: [37.3861, -122.0839], location: 'Silicon Valley, USA' }, + { + content_id: MOCK_DASHBOARD_ID, + refresh_interval: '300', + display_errors: 'false', + show_labels: 'true', + screenly_oauth_tokens_url: 'http://localhost:3000/', + screenly_app_auth_token: 'mock-token', + } + ) + +const { screenlyJsContent: reportLabelsScreenlyJsContent } = + createMockScreenlyForScreenshots( + { coordinates: [37.3861, -122.0839], location: 'Silicon Valley, USA' }, + { + content_id: MOCK_REPORT_ID, + refresh_interval: '300', + display_errors: 'false', + show_labels: 'true', + screenly_oauth_tokens_url: 'http://localhost:3000/', + screenly_app_auth_token: 'mock-token', + } + ) + +type BrowserContext = Awaited> + async function takeScreenshot( browser: Browser, width: number, height: number, filename: string, screenlyJsContent: string, - setup: (context: Awaited>) => Promise, - waitFor: ( - page: Awaited< - ReturnType>['newPage']> - > - ) => Promise + setup: (context: BrowserContext) => Promise ): Promise { const screenshotsDir = getScreenshotsDir() const context = await browser.newContext({ viewport: { width, height } }) @@ -413,7 +436,7 @@ async function takeScreenshot( await setup(context) await page.goto('/?animations=false') - await waitFor(page) + await page.waitForLoadState('networkidle') await page.screenshot({ path: path.join(screenshotsDir, filename), @@ -423,6 +446,45 @@ async function takeScreenshot( await context.close() } +function mockCredentials(context: BrowserContext): Promise { + return context.route(/access_token\//, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_CREDENTIALS), + }) + ) +} + +async function setupDashboardRoutes( + context: BrowserContext, + dashboardStatus: number +): Promise { + await mockCredentials(context) + await context.route(/analytics\/dashboards/, (route) => + route.fulfill({ + status: dashboardStatus, + contentType: 'application/json', + body: JSON.stringify( + dashboardStatus === 200 + ? MOCK_DASHBOARD_RESPONSE + : { message: dashboardStatus === 404 ? 'Not Found' : 'Unauthorized' } + ), + }) + ) +} + +async function setupReportRoutes(context: BrowserContext): Promise { + await mockCredentials(context) + await context.route(/analytics\/reports/, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_REPORT_RESPONSE), + }) + ) +} + for (const { width, height } of RESOLUTIONS) { test(`screenshot dashboard ${width}x${height}`, async ({ browser }) => { await takeScreenshot( @@ -431,25 +493,7 @@ for (const { width, height } of RESOLUTIONS) { height, `dashboard-${width}x${height}.png`, dashboardScreenlyJsContent, - async (context) => { - await context.route(/access_token\//, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_CREDENTIALS), - }) - }) - await context.route(/analytics\/dashboards/, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_DASHBOARD_RESPONSE), - }) - }) - }, - async (page) => { - await page.waitForLoadState('networkidle') - } + (context) => setupDashboardRoutes(context, 200) ) }) } @@ -465,25 +509,7 @@ for (const [width, height] of [ height, `error-${width}x${height}.png`, dashboardScreenlyJsContent, - async (context) => { - await context.route(/access_token\//, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_CREDENTIALS), - }) - }) - await context.route(/analytics\/dashboards/, async (route) => { - await route.fulfill({ - status: 401, - contentType: 'application/json', - body: JSON.stringify({ message: 'Unauthorized' }), - }) - }) - }, - async (page) => { - await page.waitForLoadState('networkidle') - } + (context) => setupDashboardRoutes(context, 401) ) }) } @@ -495,28 +521,26 @@ test('screenshot error-not-found 3840x2160', async ({ browser }) => { 2160, 'error-not-found-3840x2160.png', dashboardScreenlyJsContent, - async (context) => { - await context.route(/access_token\//, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_CREDENTIALS), - }) - }) - await context.route(/analytics\/dashboards/, async (route) => { - await route.fulfill({ - status: 404, - contentType: 'application/json', - body: JSON.stringify({ message: 'Not Found' }), - }) - }) - }, - async (page) => { - await page.waitForLoadState('networkidle') - } + (context) => setupDashboardRoutes(context, 404) ) }) +for (const [width, height] of [ + [1920, 1080], + [1080, 1920], +]) { + test(`screenshot dashboard-labels ${width}x${height}`, async ({ browser }) => { + await takeScreenshot( + browser, + width, + height, + `dashboard-labels-${width}x${height}.png`, + dashboardLabelsScreenlyJsContent, + (context) => setupDashboardRoutes(context, 200) + ) + }) +} + for (const [width, height] of [ [3840, 2160], [2160, 3840], @@ -528,25 +552,23 @@ for (const [width, height] of [ height, `report-${width}x${height}.png`, reportScreenlyJsContent, - async (context) => { - await context.route(/access_token\//, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_CREDENTIALS), - }) - }) - await context.route(/analytics\/reports/, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(MOCK_REPORT_RESPONSE), - }) - }) - }, - async (page) => { - await page.waitForLoadState('networkidle') - } + (context) => setupReportRoutes(context) + ) + }) +} + +for (const [width, height] of [ + [1920, 1080], + [1080, 1920], +]) { + test(`screenshot report-labels ${width}x${height}`, async ({ browser }) => { + await takeScreenshot( + browser, + width, + height, + `report-labels-${width}x${height}.png`, + reportLabelsScreenlyJsContent, + (context) => setupReportRoutes(context) ) }) } diff --git a/screenly.yml b/screenly.yml index f962cfe..8d30555 100644 --- a/screenly.yml +++ b/screenly.yml @@ -49,3 +49,13 @@ settings: help_text: How often to refresh Salesforce data. Defaults to 300 seconds (5 minutes). type: number schema_version: 1 + show_labels: + type: string + default_value: 'false' + title: Show Labels + optional: true + help_text: + properties: + help_text: Show data labels on charts and gauges. + type: boolean + schema_version: 1 diff --git a/screenly_qc.yml b/screenly_qc.yml index 8a681a4..ec62b6f 100644 --- a/screenly_qc.yml +++ b/screenly_qc.yml @@ -49,3 +49,13 @@ settings: help_text: How often to refresh Salesforce data. Defaults to 300 seconds (5 minutes). type: number schema_version: 1 + show_labels: + type: string + default_value: 'false' + title: Show Labels + optional: true + help_text: + properties: + help_text: Show data labels on charts and gauges. + type: boolean + schema_version: 1 diff --git a/screenshots/dashboard-1080x1920.webp b/screenshots/dashboard-1080x1920.webp index 69debb9..15ca156 100644 Binary files a/screenshots/dashboard-1080x1920.webp and b/screenshots/dashboard-1080x1920.webp differ diff --git a/screenshots/dashboard-1280x720.webp b/screenshots/dashboard-1280x720.webp index e8c7ae0..e4b645f 100644 Binary files a/screenshots/dashboard-1280x720.webp and b/screenshots/dashboard-1280x720.webp differ diff --git a/screenshots/dashboard-1920x1080.webp b/screenshots/dashboard-1920x1080.webp index 1a19109..4be54ed 100644 Binary files a/screenshots/dashboard-1920x1080.webp and b/screenshots/dashboard-1920x1080.webp differ diff --git a/screenshots/dashboard-2160x3840.webp b/screenshots/dashboard-2160x3840.webp index fdd6af5..c90e18f 100644 Binary files a/screenshots/dashboard-2160x3840.webp and b/screenshots/dashboard-2160x3840.webp differ diff --git a/screenshots/dashboard-2160x4096.webp b/screenshots/dashboard-2160x4096.webp index 5f22858..ef7ed9a 100644 Binary files a/screenshots/dashboard-2160x4096.webp and b/screenshots/dashboard-2160x4096.webp differ diff --git a/screenshots/dashboard-3840x2160.webp b/screenshots/dashboard-3840x2160.webp index db08c2e..1ebe379 100644 Binary files a/screenshots/dashboard-3840x2160.webp and b/screenshots/dashboard-3840x2160.webp differ diff --git a/screenshots/dashboard-4096x2160.webp b/screenshots/dashboard-4096x2160.webp index 1f16a89..4e3d860 100644 Binary files a/screenshots/dashboard-4096x2160.webp and b/screenshots/dashboard-4096x2160.webp differ diff --git a/screenshots/dashboard-480x800.webp b/screenshots/dashboard-480x800.webp index 3954a28..f408f9d 100644 Binary files a/screenshots/dashboard-480x800.webp and b/screenshots/dashboard-480x800.webp differ diff --git a/screenshots/dashboard-720x1280.webp b/screenshots/dashboard-720x1280.webp index e921f85..0340a90 100644 Binary files a/screenshots/dashboard-720x1280.webp and b/screenshots/dashboard-720x1280.webp differ diff --git a/screenshots/dashboard-800x480.webp b/screenshots/dashboard-800x480.webp index b8dbd30..185737e 100644 Binary files a/screenshots/dashboard-800x480.webp and b/screenshots/dashboard-800x480.webp differ diff --git a/screenshots/dashboard-labels-1080x1920.webp b/screenshots/dashboard-labels-1080x1920.webp new file mode 100644 index 0000000..69debb9 Binary files /dev/null and b/screenshots/dashboard-labels-1080x1920.webp differ diff --git a/screenshots/dashboard-labels-1920x1080.webp b/screenshots/dashboard-labels-1920x1080.webp new file mode 100644 index 0000000..1a19109 Binary files /dev/null and b/screenshots/dashboard-labels-1920x1080.webp differ diff --git a/screenshots/report-2160x3840.webp b/screenshots/report-2160x3840.webp index 4102b95..3db3263 100644 Binary files a/screenshots/report-2160x3840.webp and b/screenshots/report-2160x3840.webp differ diff --git a/screenshots/report-3840x2160.webp b/screenshots/report-3840x2160.webp index 327cd3f..252d840 100644 Binary files a/screenshots/report-3840x2160.webp and b/screenshots/report-3840x2160.webp differ diff --git a/screenshots/report-labels-1080x1920.webp b/screenshots/report-labels-1080x1920.webp new file mode 100644 index 0000000..0dfae35 Binary files /dev/null and b/screenshots/report-labels-1080x1920.webp differ diff --git a/screenshots/report-labels-1920x1080.webp b/screenshots/report-labels-1920x1080.webp new file mode 100644 index 0000000..66da504 Binary files /dev/null and b/screenshots/report-labels-1920x1080.webp differ diff --git a/src/main.ts b/src/main.ts index ab092d0..fee9f8e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,7 +23,8 @@ async function loadAndRenderContent( contentType: SalesforceContentType, instanceUrl: string, accessToken: string, - contentId: string + contentId: string, + showLabels: boolean ): Promise { if (contentType === 'dashboard') { const results = await getDashboardResults( @@ -31,12 +32,12 @@ async function loadAndRenderContent( accessToken, contentId ) - renderDashboard(results) + renderDashboard(results, showLabels) return } const results = await getReportResults(instanceUrl, accessToken, contentId) - renderReport(contentId, results) + renderReport(contentId, results, showLabels) } function handleError(message: string, displayErrors: boolean): void { @@ -49,7 +50,8 @@ async function fetchAndRender( contentType: SalesforceContentType, getRuntimeState: () => RuntimeState, refreshToken: RefreshToken, - displayErrors: boolean + displayErrors: boolean, + showLabels: boolean ): Promise { let { accessToken, instanceUrl } = getRuntimeState() const { credentialError } = getRuntimeState() @@ -63,7 +65,13 @@ async function fetchAndRender( } try { - await loadAndRenderContent(contentType, instanceUrl, accessToken, contentId) + await loadAndRenderContent( + contentType, + instanceUrl, + accessToken, + contentId, + showLabels + ) showScreen('dashboard-screen') return } catch (err) { @@ -89,7 +97,13 @@ async function fetchAndRender( return } - await loadAndRenderContent(contentType, instanceUrl, accessToken, contentId) + await loadAndRenderContent( + contentType, + instanceUrl, + accessToken, + contentId, + showLabels + ) showScreen('dashboard-screen') } catch (retryErr) { handleError( @@ -105,9 +119,11 @@ document.addEventListener('DOMContentLoaded', async () => { setupErrorHandling() const contentId = getSettingWithDefault('content_id', '') - const refreshInterval = getSettingWithDefault('refresh_interval', 300) const displayErrors = getSettingWithDefault('display_errors', 'false') === 'true' + const refreshInterval = getSettingWithDefault('refresh_interval', 300) + const showLabels = + getSettingWithDefault('show_labels', 'false') === 'true' if (!contentId) { showError('Please configure the Salesforce Content ID in settings.') @@ -157,7 +173,8 @@ document.addEventListener('DOMContentLoaded', async () => { contentType, getRuntimeState, refreshToken, - displayErrors + displayErrors, + showLabels ) await run() diff --git a/src/render/chart.ts b/src/render/chart.ts index 4e5c819..6f0efb9 100644 --- a/src/render/chart.ts +++ b/src/render/chart.ts @@ -102,7 +102,8 @@ export function renderChart( componentId: string, reportResult: ReportResult, sfType: string, - title: string + title: string, + showLabels: boolean = false ): void { const { labels, values } = extractChartData(reportResult) @@ -140,7 +141,9 @@ export function renderChart( maintainAspectRatio: false, plugins: { legend: { labels: { color: '#ffffff' } }, - datalabels: buildDatalabelsConfig(chartType), + datalabels: showLabels + ? buildDatalabelsConfig(chartType) + : { display: false }, }, scales: buildScalesConfig(chartType), }, diff --git a/src/render/gauge.ts b/src/render/gauge.ts index 00b2da6..bb5ef14 100644 --- a/src/render/gauge.ts +++ b/src/render/gauge.ts @@ -74,11 +74,43 @@ function drawGaugeBreakLabels( ctx.restore() } +function buildNeedlePlugin( + componentId: string, + min: number, + max: number, + breaks: { lowerBound: number; upperBound: number }[], + pct: number, + showLabels: boolean +) { + return { + id: `gaugeNeedle-${componentId}`, + afterDraw(chart: Chart) { + const { ctx } = chart + const arcEl = chart.getDatasetMeta(0).data[0] as ArcElement + if (!arcEl) return + if (showLabels) { + drawGaugeBreakLabels( + ctx, + arcEl.x, + arcEl.y, + arcEl.outerRadius, + arcEl.innerRadius, + min, + max, + breaks + ) + } + drawGaugeNeedle(ctx, arcEl.x, arcEl.y, arcEl.outerRadius, pct) + }, + } +} + export function renderGauge( container: HTMLElement, componentId: string, reportResult: ReportResult, - meta: DashboardMetadataComponent + meta: DashboardMetadataComponent, + showLabels: boolean = false ): void { const value = Number( reportResult.factMap?.['T!T']?.aggregates?.[0]?.value ?? 0 @@ -101,25 +133,14 @@ export function renderGauge( canvas.id = `chart-${componentId}` container.appendChild(canvas) - const needlePlugin = { - id: `gaugeNeedle-${componentId}`, - afterDraw(chart: Chart) { - const { ctx } = chart - const arcEl = chart.getDatasetMeta(0).data[0] as ArcElement - if (!arcEl) return - drawGaugeBreakLabels( - ctx, - arcEl.x, - arcEl.y, - arcEl.outerRadius, - arcEl.innerRadius, - min, - max, - breaks - ) - drawGaugeNeedle(ctx, arcEl.x, arcEl.y, arcEl.outerRadius, pct) - }, - } + const needlePlugin = buildNeedlePlugin( + componentId, + min, + max, + breaks, + pct, + showLabels + ) const gaugeConfig: ChartConfiguration<'doughnut'> = { type: 'doughnut', diff --git a/src/render/index.ts b/src/render/index.ts index 174da8d..c69ea0e 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -9,11 +9,18 @@ import { CHART_TYPES, renderChart } from './chart' import { renderGauge } from './gauge' import { renderTable } from './table' import { renderEmpty } from './utils' +import { + createChartCard, + renderMetric, + renderReportContent, + setupReportGrid, +} from './report' function renderComponent( container: HTMLElement, meta: DashboardMetadataComponent, - item: ComponentDataItem + item: ComponentDataItem, + showLabels: boolean ): void { if (!item.reportResult || item.status.componentDataStatus === 'NO_DATA') { renderEmpty(container) @@ -25,11 +32,24 @@ function renderComponent( const title = meta.header ?? meta.title ?? '' if (sfTypeLower === 'gauge') { - renderGauge(container, item.componentId, item.reportResult, meta) + renderGauge( + container, + item.componentId, + item.reportResult, + meta, + showLabels + ) } else if (sfTypeLower === 'metric') { renderMetric(container, item.reportResult, meta) } else if (CHART_TYPES.has(sfTypeLower)) { - renderChart(container, item.componentId, item.reportResult, sfType, title) + renderChart( + container, + item.componentId, + item.reportResult, + sfType, + title, + showLabels + ) } else { renderTable(container, meta, item.reportResult) } @@ -45,201 +65,10 @@ function getDashboardElements(): { } } -function createChartCard(titleText: string): { - card: HTMLDivElement - contentContainer: HTMLDivElement -} { - const card = document.createElement('div') - card.className = 'chart-card' - - const title = document.createElement('h3') - title.className = 'chart-title' - title.textContent = titleText - card.appendChild(title) - - const contentContainer = document.createElement('div') - contentContainer.className = 'chart-container' - card.appendChild(contentContainer) - - return { card, contentContainer } -} - -function appendReportCard( - chartsGrid: HTMLElement, - titleText: string, - options?: { - cardClassName?: string - containerClassName?: string - } -): HTMLDivElement { - const { card, contentContainer } = createChartCard(titleText) - card.style.gridColumn = '1 / span 12' - - if (options?.cardClassName) { - card.classList.add(options.cardClassName) - } - - if (options?.containerClassName) { - contentContainer.classList.add(options.containerClassName) - } - - chartsGrid.appendChild(card) - return contentContainer -} - -function renderStat( - container: HTMLElement, - label: string, - value: string -): void { - const stat = document.createElement('div') - stat.className = 'report-stat' - - const statValue = document.createElement('div') - statValue.className = 'report-stat-value' - statValue.textContent = value - - const statLabel = document.createElement('div') - statLabel.className = 'report-stat-label' - statLabel.textContent = label - - stat.appendChild(statValue) - stat.appendChild(statLabel) - container.appendChild(stat) -} - -function renderMetric( - container: HTMLElement, - reportResult: ReportResult, - _meta: DashboardMetadataComponent -): void { - const entry = reportResult.factMap?.['T!T'] - - if (!entry) { - renderEmpty(container) - return - } - - const value = entry.aggregates?.[0]?.value ?? 0 - - const el = document.createElement('div') - el.className = 'metric-value' - el.textContent = Number(value).toLocaleString() - container.appendChild(el) -} - -function hasReportDetailRows(reportResult: ReportResult): boolean { - const rows = Object.values(reportResult.factMap ?? {}).flatMap( - (entry) => entry.rows ?? [] - ) - const detailColumns = reportResult.reportMetadata?.detailColumns ?? [] - - return ( - (reportResult.hasDetailRows ?? true) && - rows.length > 0 && - detailColumns.length > 0 - ) -} - -function hasGroupedReportData(reportResult: ReportResult): boolean { - const groupings = reportResult.groupingsDown?.groupings ?? [] - const keys = Object.keys(reportResult.factMap ?? {}) - - return groupings.length > 0 && keys.some((key) => key !== 'T!T') -} - -function getReportChartType(reportResult: ReportResult): string | null { - const chartType = reportResult.reportMetadata?.chart?.chartType - - return chartType ? chartType : null -} - -function setupReportGrid( - chartsGrid: HTMLElement, - dashboardTitle: HTMLElement | null, - reportName: string +export function renderDashboard( + results: DashboardResults, + showLabels: boolean = false ): void { - if (dashboardTitle) { - dashboardTitle.textContent = reportName - } - - chartsGrid.innerHTML = '' - chartsGrid.style.gridTemplateColumns = 'repeat(12, 1fr)' - chartsGrid.style.gridAutoRows = 'minmax(6rem, auto)' -} - -function renderReportChartCard( - chartsGrid: HTMLElement, - contentId: string, - reportResult: ReportResult, - reportName: string, - chartType: string = 'Column' -): boolean { - if (!hasGroupedReportData(reportResult)) { - return false - } - - const chartContainer = appendReportCard(chartsGrid, reportName, { - cardClassName: 'report-chart-card', - containerClassName: 'report-chart-container', - }) - - renderChart(chartContainer, contentId, reportResult, chartType, reportName) - return true -} - -function renderReportTableCard( - chartsGrid: HTMLElement, - contentId: string, - reportResult: ReportResult, - reportName: string -): boolean { - if (!hasReportDetailRows(reportResult)) { - return false - } - - const meta = { - id: contentId, - header: reportName, - title: reportName, - reportId: contentId, - type: 'Report', - properties: { - visualizationType: 'FlexTable', - visualizationProperties: {}, - aggregates: [], - groupings: null, - }, - } - - const tableContainer = appendReportCard(chartsGrid, `${reportName} Details`, { - cardClassName: 'report-table-card', - }) - - renderTable(tableContainer, meta, reportResult) - return true -} - -function renderReportFallbackCard( - chartsGrid: HTMLElement, - reportName: string, - aggregateLabel: string, - aggregateValue: number | null -): void { - if (aggregateValue !== null) { - const statContainer = appendReportCard(chartsGrid, reportName, { - cardClassName: 'report-stat-card', - }) - - renderStat(statContainer, aggregateLabel, String(aggregateValue)) - return - } - - const emptyContainer = appendReportCard(chartsGrid, reportName) - renderEmpty(emptyContainer) -} - -export function renderDashboard(results: DashboardResults): void { const { dashboardTitle, chartsGrid } = getDashboardElements() if (!chartsGrid) return @@ -283,53 +112,29 @@ export function renderDashboard(results: DashboardResults): void { card.style.gridRow = `${pos.row + 1} / span ${pos.rowspan}` } - renderComponent(contentContainer, meta, item) + renderComponent(contentContainer, meta, item, showLabels) chartsGrid.appendChild(card) } } export function renderReport( contentId: string, - reportResult: ReportResult + reportResult: ReportResult, + showLabels: boolean = false ): void { const { dashboardTitle, chartsGrid } = getDashboardElements() if (!chartsGrid) return const reportName = reportResult.reportMetadata?.name ?? `Report ${contentId}` - const aggregateLabel = - reportResult.factMap?.['T!T']?.aggregates?.[0]?.label ?? - reportResult.reportMetadata?.aggregates?.[0] ?? - 'Value' - const aggregateValue = - reportResult.factMap?.['T!T']?.aggregates?.[0]?.value ?? null - const reportChartType = getReportChartType(reportResult) setupReportGrid(chartsGrid, dashboardTitle, reportName) - - const renderedChart = renderReportChartCard( + renderReportContent( chartsGrid, contentId, reportResult, reportName, - reportChartType ?? 'Column' - ) - const renderedTable = renderReportTableCard( - chartsGrid, - contentId, - reportResult, - reportName - ) - - if (renderedChart || renderedTable) { - return - } - - renderReportFallbackCard( - chartsGrid, - reportName, - aggregateLabel, - aggregateValue + showLabels ) } diff --git a/src/render/report.ts b/src/render/report.ts new file mode 100644 index 0000000..3d759b6 --- /dev/null +++ b/src/render/report.ts @@ -0,0 +1,245 @@ +import type { DashboardMetadataComponent, ReportResult } from '../types' +import { renderChart } from './chart' +import { renderTable } from './table' +import { renderEmpty } from './utils' + +export function createChartCard(titleText: string): { + card: HTMLDivElement + contentContainer: HTMLDivElement +} { + const card = document.createElement('div') + card.className = 'chart-card' + + const title = document.createElement('h3') + title.className = 'chart-title' + title.textContent = titleText + card.appendChild(title) + + const contentContainer = document.createElement('div') + contentContainer.className = 'chart-container' + card.appendChild(contentContainer) + + return { card, contentContainer } +} + +export function appendReportCard( + chartsGrid: HTMLElement, + titleText: string, + options?: { + cardClassName?: string + containerClassName?: string + } +): HTMLDivElement { + const { card, contentContainer } = createChartCard(titleText) + card.style.gridColumn = '1 / span 12' + + if (options?.cardClassName) { + card.classList.add(options.cardClassName) + } + + if (options?.containerClassName) { + contentContainer.classList.add(options.containerClassName) + } + + chartsGrid.appendChild(card) + return contentContainer +} + +function renderStat( + container: HTMLElement, + label: string, + value: string +): void { + const stat = document.createElement('div') + stat.className = 'report-stat' + + const statValue = document.createElement('div') + statValue.className = 'report-stat-value' + statValue.textContent = value + + const statLabel = document.createElement('div') + statLabel.className = 'report-stat-label' + statLabel.textContent = label + + stat.appendChild(statValue) + stat.appendChild(statLabel) + container.appendChild(stat) +} + +export function renderMetric( + container: HTMLElement, + reportResult: ReportResult, + _meta: DashboardMetadataComponent +): void { + const entry = reportResult.factMap?.['T!T'] + + if (!entry) { + renderEmpty(container) + return + } + + const value = entry.aggregates?.[0]?.value ?? 0 + + const el = document.createElement('div') + el.className = 'metric-value' + el.textContent = Number(value).toLocaleString() + container.appendChild(el) +} + +function hasReportDetailRows(reportResult: ReportResult): boolean { + const rows = Object.values(reportResult.factMap ?? {}).flatMap( + (entry) => entry.rows ?? [] + ) + const detailColumns = reportResult.reportMetadata?.detailColumns ?? [] + + return ( + (reportResult.hasDetailRows ?? true) && + rows.length > 0 && + detailColumns.length > 0 + ) +} + +function hasGroupedReportData(reportResult: ReportResult): boolean { + const groupings = reportResult.groupingsDown?.groupings ?? [] + const keys = Object.keys(reportResult.factMap ?? {}) + + return groupings.length > 0 && keys.some((key) => key !== 'T!T') +} + +function getReportChartType(reportResult: ReportResult): string | null { + const chartType = reportResult.reportMetadata?.chart?.chartType + + return chartType ? chartType : null +} + +export function setupReportGrid( + chartsGrid: HTMLElement, + dashboardTitle: HTMLElement | null, + reportName: string +): void { + if (dashboardTitle) { + dashboardTitle.textContent = reportName + } + + chartsGrid.innerHTML = '' + chartsGrid.style.gridTemplateColumns = 'repeat(12, 1fr)' + chartsGrid.style.gridAutoRows = 'minmax(6rem, auto)' +} + +function renderReportChartCard( + chartsGrid: HTMLElement, + contentId: string, + reportResult: ReportResult, + reportName: string, + chartType: string = 'Column', + showLabels: boolean = false +): boolean { + if (!hasGroupedReportData(reportResult)) { + return false + } + + const chartContainer = appendReportCard(chartsGrid, reportName, { + cardClassName: 'report-chart-card', + containerClassName: 'report-chart-container', + }) + + renderChart( + chartContainer, + contentId, + reportResult, + chartType, + reportName, + showLabels + ) + return true +} + +function renderReportTableCard( + chartsGrid: HTMLElement, + contentId: string, + reportResult: ReportResult, + reportName: string +): boolean { + if (!hasReportDetailRows(reportResult)) { + return false + } + + const meta: DashboardMetadataComponent = { + id: contentId, + header: reportName, + title: reportName, + reportId: contentId, + type: 'Report', + properties: { + visualizationType: 'FlexTable', + visualizationProperties: {}, + aggregates: [], + groupings: null, + }, + } + + const tableContainer = appendReportCard(chartsGrid, `${reportName} Details`, { + cardClassName: 'report-table-card', + }) + + renderTable(tableContainer, meta, reportResult) + return true +} + +function renderReportFallbackCard( + chartsGrid: HTMLElement, + reportName: string, + aggregateLabel: string, + aggregateValue: number | null +): void { + if (aggregateValue !== null) { + const statContainer = appendReportCard(chartsGrid, reportName, { + cardClassName: 'report-stat-card', + }) + + renderStat(statContainer, aggregateLabel, String(aggregateValue)) + return + } + + const emptyContainer = appendReportCard(chartsGrid, reportName) + renderEmpty(emptyContainer) +} + +export function renderReportContent( + chartsGrid: HTMLElement, + contentId: string, + reportResult: ReportResult, + reportName: string, + showLabels: boolean +): void { + const renderedChart = renderReportChartCard( + chartsGrid, + contentId, + reportResult, + reportName, + getReportChartType(reportResult) ?? 'Column', + showLabels + ) + const renderedTable = renderReportTableCard( + chartsGrid, + contentId, + reportResult, + reportName + ) + + if (renderedChart || renderedTable) return + + const aggregateLabel = + reportResult.factMap?.['T!T']?.aggregates?.[0]?.label ?? + reportResult.reportMetadata?.aggregates?.[0] ?? + 'Value' + const aggregateValue = + reportResult.factMap?.['T!T']?.aggregates?.[0]?.value ?? null + + renderReportFallbackCard( + chartsGrid, + reportName, + aggregateLabel, + aggregateValue + ) +}