Private/Report/Get-NMMReportHtmlTemplate.ps1

function Get-NMMReportHtmlTemplate {
    <#
    .SYNOPSIS
        Generates the complete HTML report from a report builder.
    .DESCRIPTION
        Renders all sections into a self-contained HTML document with
        embedded CSS, JavaScript libraries (via CDN), and Nerdio branding.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [NMMReportBuilder]$ReportBuilder,

        [Parameter(Mandatory = $true)]
        [hashtable]$Assets
    )

    $colors = $ReportBuilder.GetBrandColors()

    # Generate sections HTML
    $sectionsHtml = foreach ($section in ($ReportBuilder.Sections | Sort-Object Order)) {
        Get-NMMSectionHtml -Section $section -Colors $colors
    }

    # Logo handling
    $logoHtml = if ($ReportBuilder.LogoSource -eq 'url' -and $ReportBuilder.LogoData) {
        "<img src=`"$([System.Web.HttpUtility]::HtmlAttributeEncode($ReportBuilder.LogoData))`" alt=`"Logo`" class=`"nmm-logo`">"
    }
    elseif ($Assets.Logo) {
        "<img src=`"data:image/png;base64,$($Assets.Logo)`" alt=`"Nerdio`" class=`"nmm-logo`">"
    }
    else {
        ""
    }

    # Timestamp
    $timestampHtml = if ($ReportBuilder.IncludeTimestamp) {
        "<p class='text-white-50 mb-0 small'>Generated: $($ReportBuilder.GeneratedAt.ToString('yyyy-MM-dd HH:mm:ss'))</p>"
    }
    else { "" }

    # Subtitle
    $subtitleHtml = if ($ReportBuilder.Subtitle) {
        "<p class='lead mb-2'>$([System.Web.HttpUtility]::HtmlEncode($ReportBuilder.Subtitle))</p>"
    }
    else { "" }

    # Theme class
    $themeClass = if ($ReportBuilder.Theme -eq 'dark') { 'nmm-dark' } else { '' }

    return @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$([System.Web.HttpUtility]::HtmlEncode($ReportBuilder.Title))</title>
 
    <!-- Bootstrap 5 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
 
    <!-- DataTables Bootstrap 5 -->
    <link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet">
 
    <!-- Poppins Font -->
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
    <style>
$($Assets.CSS)
    </style>
</head>
<body class="$themeClass">
    <header class="nmm-header">
        <div class="container">
            $logoHtml
            <h1>$([System.Web.HttpUtility]::HtmlEncode($ReportBuilder.Title))</h1>
            $subtitleHtml
            $timestampHtml
        </div>
    </header>
 
    <main class="container">
        $($sectionsHtml -join "`n")
    </main>
 
    <footer class="nmm-footer">
        <div class="container">
            <p>$([System.Web.HttpUtility]::HtmlEncode($ReportBuilder.FooterText))</p>
            <p class="text-muted small mb-0">Powered by NMM-PS Module | <a href="https://github.com/Get-Nerdio/NMM-PS" target="_blank" class="text-muted">GitHub</a></p>
        </div>
    </footer>
 
    <!-- Scripts -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
 
    <script>
$($Assets.JavaScript)
    </script>
</body>
</html>
"@

}

function Get-NMMSectionHtml {
    <#
    .SYNOPSIS
        Generates HTML for a single report section.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [NMMReportSection]$Section,

        [Parameter(Mandatory = $true)]
        [hashtable]$Colors
    )

    $sectionId = "section-$($Section.Order)"
    $chartId = "chart-$($Section.Order)"

    # Description HTML
    $descriptionHtml = if ($Section.Description) {
        "<div class='nmm-section-description'>$([System.Web.HttpUtility]::HtmlEncode($Section.Description))</div>"
    }
    else { "" }

    # Chart HTML
    $chartHtml = ""
    $chartScript = ""
    if ($Section.ShowChart -and $Section.Data) {
        $chartData = Get-NMMChartData -Data $Section.Data -ChartType $Section.ChartType -ChartConfig $Section.ChartConfig
        if ($chartData) {
            $chartHtml = "<div id='$chartId' class='nmm-chart-container'></div>"
            $chartScript = @"
<script>
document.addEventListener('DOMContentLoaded', function() {
    initNMMChart('$chartId', $chartData);
});
</script>
"@

        }
    }

    # Table HTML
    $tableHtml = ""
    if ($Section.ShowDataTable -and $Section.Data) {
        $tableHtml = ConvertTo-NMMTableHtml -Data $Section.Data
    }

    # Custom HTML
    $customHtml = if ($Section.CustomHtml) { $Section.CustomHtml } else { "" }

    return @"
<section class="nmm-section" id="$sectionId">
    <div class="nmm-section-header">$([System.Web.HttpUtility]::HtmlEncode($Section.Title))</div>
    $descriptionHtml
    <div class="nmm-section-body">
        $chartHtml
        $tableHtml
        $customHtml
    </div>
</section>
$chartScript
"@

}

function ConvertTo-NMMTableHtml {
    <#
    .SYNOPSIS
        Converts data to an HTML table with DataTables support.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Data
    )

    if (-not $Data) { return "" }

    # Ensure array
    $dataArray = @($Data)
    if ($dataArray.Count -eq 0) { return "<p class='text-muted'>No data available.</p>" }

    # Get template for column configuration
    $templateInfo = Get-NMMReportTemplate -InputObject $dataArray[0]
    $template = $templateInfo.Template

    $columns = $template.tableColumns
    $headers = $template.columnHeaders

    # Build header row
    $headerCells = foreach ($col in $columns) {
        $headerText = if ($headers -and $headers.ContainsKey($col)) { $headers[$col] } else { $col }
        "<th>$([System.Web.HttpUtility]::HtmlEncode($headerText))</th>"
    }

    # Build data rows
    $dataRows = foreach ($item in $dataArray) {
        $cells = foreach ($col in $columns) {
            $value = $item.$col
            $displayValue = if ($null -eq $value) {
                '-'
            }
            elseif ($value -is [datetime]) {
                $value.ToString('yyyy-MM-dd HH:mm:ss')
            }
            elseif ($value -is [bool]) {
                if ($value) { '<span class="badge bg-success">Yes</span>' } else { '<span class="badge bg-secondary">No</span>' }
            }
            else {
                [System.Web.HttpUtility]::HtmlEncode($value.ToString())
            }
            "<td>$displayValue</td>"
        }
        "<tr>$($cells -join '')</tr>"
    }

    return @"
<div class="table-responsive">
    <table class="table table-hover nmm-table nmm-datatable">
        <thead>
            <tr>$($headerCells -join '')</tr>
        </thead>
        <tbody>
            $($dataRows -join "`n ")
        </tbody>
    </table>
</div>
"@

}

function Get-NMMChartData {
    <#
    .SYNOPSIS
        Generates ApexCharts configuration JSON from data.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Data,

        [Parameter(Mandatory = $true)]
        [string]$ChartType,

        [Parameter()]
        [hashtable]$ChartConfig
    )

    $dataArray = @($Data)
    if ($dataArray.Count -eq 0) { return $null }

    # Get template for chart configuration
    $templateInfo = Get-NMMReportTemplate -InputObject $dataArray[0]
    $defaultChart = $templateInfo.Template.defaultChart

    # Merge with provided config
    $config = if ($ChartConfig -and $ChartConfig.Count -gt 0) { $ChartConfig } else { $defaultChart }

    if (-not $config -or -not $config.enabled) { return $null }

    $chartTitle = if ($config.title) { $config.title } else { "" }

    switch ($ChartType) {
        { $_ -in @('pie', 'donut') } {
            # Group data by field
            $groupField = $config.groupField
            if (-not $groupField) { return $null }

            $grouped = $dataArray | Group-Object -Property $groupField
            $labels = @($grouped | ForEach-Object { if ($_.Name) { $_.Name } else { '(Empty)' } })
            $series = @($grouped | ForEach-Object { $_.Count })

            $labelsJson = $labels | ConvertTo-Json -Compress
            $seriesJson = $series | ConvertTo-Json -Compress

            return @"
{
    chart: {
        type: '$ChartType',
        height: 350
    },
    series: $seriesJson,
    labels: $labelsJson,
    title: {
        text: '$([System.Web.HttpUtility]::JavaScriptStringEncode($chartTitle))',
        align: 'center'
    },
    plotOptions: {
        pie: {
            donut: {
                size: '$( if ($ChartType -eq 'donut') { '55%' } else { '0%' } )'
            }
        }
    },
    responsive: [{
        breakpoint: 480,
        options: {
            chart: { width: 300 },
            legend: { position: 'bottom' }
        }
    }]
}
"@

        }

        { $_ -in @('bar', 'line', 'area') } {
            # Use label and value fields
            $labelField = $config.labelField
            $valueField = $config.valueField

            if (-not $labelField -or -not $valueField) {
                # Try to use first string and first numeric property
                $firstItem = $dataArray[0]
                $props = $firstItem.PSObject.Properties
                $labelField = ($props | Where-Object { $_.Value -is [string] } | Select-Object -First 1).Name
                $valueField = ($props | Where-Object { $_.Value -is [int] -or $_.Value -is [double] } | Select-Object -First 1).Name

                if (-not $labelField -or -not $valueField) { return $null }
            }

            $categories = @($dataArray | ForEach-Object { $_.$labelField })
            $values = @($dataArray | ForEach-Object { $_.$valueField })

            $categoriesJson = $categories | ConvertTo-Json -Compress
            $valuesJson = $values | ConvertTo-Json -Compress

            return @"
{
    chart: {
        type: '$ChartType',
        height: 350
    },
    series: [{
        name: '$([System.Web.HttpUtility]::JavaScriptStringEncode($valueField))',
        data: $valuesJson
    }],
    xaxis: {
        categories: $categoriesJson,
        labels: {
            rotate: -45,
            rotateAlways: false
        }
    },
    title: {
        text: '$([System.Web.HttpUtility]::JavaScriptStringEncode($chartTitle))',
        align: 'center'
    },
    plotOptions: {
        bar: {
            borderRadius: 4,
            horizontal: false
        }
    },
    dataLabels: {
        enabled: false
    }
}
"@

        }

        default {
            return $null
        }
    }
}