Private/New-SPCStorageDashboardHtmlInternal.ps1

function New-SPCStorageDashboardHtmlInternal {
    <#
    .SYNOPSIS
        Generates a 100% self-contained HTML executive ROI dashboard for storage optimization.
    .DESCRIPTION
        Zero external CDN dependencies, embedded CSS and JavaScript, SVG indicators, dynamic ROI slider.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]$OutputPath,
        [Parameter(Mandatory)] [PSCustomObject[]]$StorageWasteData,
        [Parameter()] [PSCustomObject[]]$InactiveSiteData,
        [Parameter()] [PSCustomObject[]]$VersionWasteData,
        [Parameter()] [PSCustomObject[]]$PreservationHoldData,
        [Parameter(Mandatory)] [double]$UnitPricePerGB,
        [Parameter()] [string]$CompanyLogoUrl,
        [Parameter()] [string]$ClientName
    )

    $totalTenantUsedMB = ($StorageWasteData | Measure-Object -Property StorageUsedMB -Sum).Sum
    $totalRecycleBinMB = ($StorageWasteData | Measure-Object -Property TotalRecycleBinMB -Sum).Sum
    $totalVersionWasteMB = ($StorageWasteData | Measure-Object -Property VersionWasteMB -Sum).Sum
    $totalInactiveMB = if ($InactiveSiteData) { ($InactiveSiteData | Measure-Object -Property StorageUsedMB -Sum).Sum } else { 0.0 }
    $totalPHLMB = if ($PreservationHoldData) { ($PreservationHoldData | Measure-Object -Property PHLSizeMB -Sum).Sum } else { 0.0 }
    
    $totalWasteMB = $totalRecycleBinMB + $totalVersionWasteMB
    $totalWasteGB = [Math]::Round(($totalWasteMB / 1024), 2)
    $monthlySavings = [Math]::Round(($totalWasteGB * $UnitPricePerGB), 2)
    $annualSavings = [Math]::Round(($monthlySavings * 12), 2)

    $clientDisplay = if ([string]::IsNullOrWhiteSpace($ClientName)) { "M365 Tenant Storage Audit" } else { $ClientName }
    $scanDate = (Get-Date).ToString("MMMM dd, yyyy HH:mm UTC")

    $logoHtml = if (-not [string]::IsNullOrWhiteSpace($CompanyLogoUrl)) {
        "<img src='$CompanyLogoUrl' alt='Company Logo' style='max-height:48px; max-width:200px; object-fit:contain;' />"
    } else {
        ""
    }

    $cleanSites = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($s in $StorageWasteData) {
        $cleanSites.Add([PSCustomObject]@{
            SiteUrl                   = $s.SiteUrl
            SiteTitle                 = $s.SiteTitle
            StorageUsedMB             = $s.StorageUsedMB
            TotalRecycleBinMB         = $s.TotalRecycleBinMB
            VersionWasteMB            = $s.VersionWasteMB
            TotalWasteMB              = $s.TotalWasteMB
            PotentialMonthlySavingUSD = $s.PotentialMonthlySavingUSD
        })
    }

    $jsonData = @{
        totalWasteGB    = $totalWasteGB
        unitPricePerGB  = $UnitPricePerGB
        monthlySavings  = $monthlySavings
        annualSavings   = $annualSavings
        recycleBinMB    = $totalRecycleBinMB
        versionWasteMB  = $totalVersionWasteMB
        inactiveMB      = $totalInactiveMB
        phlMB           = $totalPHLMB
        sites           = $cleanSites
    } | ConvertTo-Json -Depth 5 -Compress

    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPClean — Storage Optimization & ROI Executive Dashboard</title>
<style>
  :root {
    --bg-dark: #0f172a; --card-bg: #1e293b; --accent: #38bdf8; --accent-green: #22c55e;
    --accent-orange: #f97316; --accent-red: #ef4444; --text-light: #f8fafc; --text-dim: #94a3b8;
    --border-color: #334155;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
  body { background-color: var(--bg-dark); color: var(--text-light); padding: 24px; }
  .header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 16px; margin-bottom: 24px; }
  .title-group h1 { font-size: 24px; font-weight: 700; color: var(--text-light); }
  .title-group p { font-size: 14px; color: var(--text-dim); }
  .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-bottom: 24px; }
  .kpi-card { background: var(--card-bg); padding: 20px; border-radius: 12px; border: 1px solid var(--border-color); }
  .kpi-title { font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
  .kpi-value { font-size: 28px; font-weight: 700; color: var(--text-light); }
  .kpi-value.green { color: var(--accent-green); }
  .kpi-value.orange { color: var(--accent-orange); }
  .kpi-sub { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
  .dashboard-main { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; margin-bottom: 24px; }
  .card { background: var(--card-bg); padding: 20px; border-radius: 12px; border: 1px solid var(--border-color); }
  .card h2 { font-size: 18px; margin-bottom: 16px; color: var(--accent); }
  .table-wrap { overflow-x: auto; max-height: 400px; }
  table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }
  th, td { padding: 10px 12px; border-bottom: 1px solid var(--border-color); }
  th { background: #0f172a; color: var(--text-dim); font-weight: 600; position: sticky; top: 0; }
  tr:hover { background: #334155; }
  .slider-container { padding: 12px 0; }
  .slider-container input[type=range] { width: 100%; height: 6px; border-radius: 3px; background: var(--border-color); outline: none; }
  .slider-labels { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-dim); margin-top: 8px; }
  .calc-box { background: #0f172a; border-radius: 8px; padding: 16px; margin-top: 16px; text-align: center; }
</style>
</head>
<body>
<div class="header">
  <div class="title-group">
    <h1>SPClean — Storage ROI Executive Dashboard</h1>
    <p>$clientDisplay | Generated: $scanDate</p>
  </div>
  <div>$logoHtml</div>
</div>
 
<div class="kpi-grid">
  <div class="kpi-card">
    <div class="kpi-title">Total Tenant Storage Used</div>
    <div class="kpi-value">$([Math]::Round($totalTenantUsedMB / 1024, 2)) GB</div>
    <div class="kpi-sub">$($StorageWasteData.Count) Sites Scanned</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-title">Digital Waste Identified</div>
    <div class="kpi-value orange">$totalWasteGB GB</div>
    <div class="kpi-sub">Recycle Bin + Version Sprawl</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-title">Monthly Cost Avoidance</div>
    <div class="kpi-value green">`$$monthlySavings USD</div>
    <div class="kpi-sub">Based on `$$UnitPricePerGB/GB/month</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-title">Annual Projected ROI</div>
    <div class="kpi-value green">`$$annualSavings USD</div>
    <div class="kpi-sub">Net Annualized Savings</div>
  </div>
</div>
 
<div class="dashboard-main">
  <div class="card">
    <h2>Top Storage Waste Sites</h2>
    <div class="table-wrap">
      <table id="wasteTable">
        <thead>
          <tr>
            <th>Site URL</th>
            <th>Used (MB)</th>
            <th>Recycle Bin (MB)</th>
            <th>Version Waste (MB)</th>
            <th>Total Waste (MB)</th>
            <th>Monthly Saving ($)</th>
          </tr>
        </thead>
        <tbody id="wasteBody">
        </tbody>
      </table>
    </div>
  </div>
 
  <div class="card">
    <h2>Interactive ROI Calculator</h2>
    <div class="slider-container">
      <label style="font-size:13px; color:var(--text-dim);">Target Remediation Goal: <span id="remediationGoalText" style="color:var(--accent); font-weight:bold;">100%</span></label>
      <input type="range" id="goalSlider" min="10" max="100" value="100" step="5" oninput="updateCalculator(this.value)">
      <div class="slider-labels"><span>10%</span><span>50%</span><span>100%</span></div>
    </div>
    <div class="calc-box">
      <div style="font-size:13px; color:var(--text-dim);">Estimated Annual Savings</div>
      <div id="dynAnnualSavings" style="font-size:32px; font-weight:bold; color:var(--accent-green); margin-top:8px;">`$$annualSavings USD</div>
      <div id="dynStorageFreed" style="font-size:13px; color:var(--accent); margin-top:4px;">$totalWasteGB GB to be freed</div>
    </div>
  </div>
</div>
 
<script>
  const appData = $jsonData;
 
  function renderTable() {
    const tbody = document.getElementById('wasteBody');
    tbody.innerHTML = '';
    if (!appData.sites || appData.sites.length === 0) return;
    appData.sites.slice(0, 20).forEach(s => {
      const tr = document.createElement('tr');
      tr.innerHTML = '<td><a href="' + s.SiteUrl + '" target="_blank" style="color:var(--accent);text-decoration:none;">' + s.SiteUrl + '</a></td>' +
        '<td>' + s.StorageUsedMB + '</td>' +
        '<td>' + s.TotalRecycleBinMB + '</td>' +
        '<td>' + s.VersionWasteMB + '</td>' +
        '<td style="font-weight:bold;color:var(--accent-orange);">' + s.TotalWasteMB + '</td>' +
        '<td style="color:var(--accent-green);font-weight:bold;">$' + s.PotentialMonthlySavingUSD + '</td>';
      tbody.appendChild(tr);
    });
  }
 
  function updateCalculator(val) {
    document.getElementById('remediationGoalText').innerText = val + '%';
    const ratio = val / 100.0;
    const freedGB = (appData.totalWasteGB * ratio).toFixed(2);
    const monthly = (freedGB * appData.unitPricePerGB).toFixed(2);
    const annual = (monthly * 12).toFixed(2);
    document.getElementById('dynAnnualSavings').innerText = '$' + annual + ' USD';
    document.getElementById('dynStorageFreed').innerText = freedGB + ' GB to be freed';
  }
 
  renderTable();
</script>
</body>
</html>
"@


    $parentDir = Split-Path $OutputPath -Parent
    if (-not [string]::IsNullOrWhiteSpace($parentDir) -and -not (Test-Path $parentDir)) {
        New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
    }

    $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
}