Private/New-HtmlDashboard.ps1

function New-HtmlDashboard {
    <#
    .SYNOPSIS
        Generates a dark-themed HTML dashboard for a user lookup report.
    .DESCRIPTION
        Takes consolidated user data from Get-UserEverything and renders an HTML
        dashboard with sections for AD details, M365 licensing, devices, and
        sign-in history. Uses an orange (#f0883e) accent color.
    .PARAMETER UserData
        The consolidated PSCustomObject returned by Get-UserEverything.
    .PARAMETER OutputPath
        Full file path for the generated HTML report.
    .OUTPUTS
        System.IO.FileInfo for the generated HTML file.
    .EXAMPLE
        New-HtmlDashboard -UserData $userData -OutputPath "C:\Reports\jsmith.html"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$UserData,

        [Parameter(Mandatory)]
        [string]$OutputPath
    )

    $ad      = $UserData.ADAccount
    $m365    = $UserData.M365
    $devices = $UserData.Devices
    $signIns = $UserData.SignIns

    $displayName = if ($ad) { $ad.DisplayName } else { $UserData.Identity }
    $title       = if ($ad) { $ad.Title } else { 'N/A' }
    $department  = if ($ad) { $ad.Department } else { 'N/A' }
    $manager     = if ($ad) { $ad.Manager } else { 'N/A' }
    $email       = if ($ad) { $ad.Email } else { 'N/A' }
    $upn         = if ($ad) { $ad.UPN } else { 'N/A' }
    $sam         = if ($ad) { $ad.SAMAccountName } else { 'N/A' }
    $office      = if ($ad) { $ad.Office } else { 'N/A' }

    # Status badges
    $enabledBadge = if ($ad -and $ad.Enabled) {
        '<span class="badge badge-good">Enabled</span>'
    } else {
        '<span class="badge badge-bad">Disabled</span>'
    }

    $lockedBadge = if ($ad -and $ad.LockedOut) {
        '<span class="badge badge-bad">Locked Out</span>'
    } elseif ($ad) {
        '<span class="badge badge-good">Not Locked</span>'
    } else { '' }

    $licenseBadge = if ($m365 -and $m365.LicenseAssignments -and $m365.LicenseAssignments.Count -gt 0) {
        '<span class="badge badge-good">Licensed</span>'
    } elseif ($m365) {
        '<span class="badge badge-bad">Unlicensed</span>'
    } else {
        '<span class="badge badge-warn">M365 Unavailable</span>'
    }

    $mfaBadge = if ($m365 -and $m365.MFAStatus -eq 'Enabled') {
        '<span class="badge badge-good">MFA On</span>'
    } elseif ($m365) {
        '<span class="badge badge-bad">MFA Off</span>'
    } else { '' }

    # Build AD details table
    $adSection = if ($ad) {
        $pwdLastSet = if ($ad.PasswordLastSet) { $ad.PasswordLastSet.ToString('yyyy-MM-dd HH:mm') } else { 'Never' }
        $lastLogon  = if ($ad.LastLogonDate) { $ad.LastLogonDate.ToString('yyyy-MM-dd HH:mm') } else { 'Never' }
        $created    = if ($ad.Created) { $ad.Created.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' }
        $expiration = if ($ad.AccountExpirationDate) { $ad.AccountExpirationDate.ToString('yyyy-MM-dd') } else { 'Never' }
        $groups     = if ($ad.MemberOf) { ($ad.MemberOf | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None</li>' }
        $reports    = if ($ad.DirectReports) { ($ad.DirectReports | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None</li>' }

        @"
        <div class="card">
            <h2>Active Directory Details</h2>
            <div class="detail-grid">
                <div class="detail-row"><span class="label">SAMAccountName</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.SAMAccountName))</span></div>
                <div class="detail-row"><span class="label">UPN</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.UPN))</span></div>
                <div class="detail-row"><span class="label">Distinguished Name</span><span class="value" style="font-size:0.85em;">$([System.Web.HttpUtility]::HtmlEncode($ad.DistinguishedName))</span></div>
                <div class="detail-row"><span class="label">Description</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.Description))</span></div>
                <div class="detail-row"><span class="label">Account Status</span><span class="value">$enabledBadge $lockedBadge</span></div>
                <div class="detail-row"><span class="label">Password Last Set</span><span class="value">$pwdLastSet</span></div>
                <div class="detail-row"><span class="label">Password Expired</span><span class="value">$($ad.PasswordExpired)</span></div>
                <div class="detail-row"><span class="label">Password Never Expires</span><span class="value">$($ad.PasswordNeverExpires)</span></div>
                <div class="detail-row"><span class="label">Last Logon</span><span class="value">$lastLogon</span></div>
                <div class="detail-row"><span class="label">Account Created</span><span class="value">$created</span></div>
                <div class="detail-row"><span class="label">Account Expires</span><span class="value">$expiration</span></div>
            </div>
            <h3>Group Memberships</h3>
            <ul class="item-list">$groups</ul>
            <h3>Direct Reports</h3>
            <ul class="item-list">$reports</ul>
        </div>
"@

    }
    else {
        '<div class="card"><h2>Active Directory Details</h2><p class="unavailable">AD data unavailable.</p></div>'
    }

    # Build M365 section
    $m365Section = if ($m365) {
        $licenses = if ($m365.LicenseAssignments -and $m365.LicenseAssignments.Count -gt 0) {
            ($m365.LicenseAssignments | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n"
        } else { '<li>None assigned</li>' }

        $mfaMethods = if ($m365.MFAMethods -and $m365.MFAMethods.Count -gt 0) {
            ($m365.MFAMethods | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n"
        } else { '<li>None configured</li>' }

        $caPolicies = if ($m365.ConditionalAccessPolicies -and $m365.ConditionalAccessPolicies.Count -gt 0) {
            ($m365.ConditionalAccessPolicies | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n"
        } else { '<li>None detected</li>' }

        $sharedMbx = if ($m365.SharedMailboxAccess -and $m365.SharedMailboxAccess.Count -gt 0) {
            ($m365.SharedMailboxAccess | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n"
        } else { '<li>None</li>' }

        @"
        <div class="card">
            <h2>Microsoft 365 Details $licenseBadge $mfaBadge</h2>
            <div class="detail-grid">
                <div class="detail-row"><span class="label">Mailbox Size</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.MailboxSize))</span></div>
                <div class="detail-row"><span class="label">Mailbox Item Count</span><span class="value">$($m365.MailboxItemCount)</span></div>
                <div class="detail-row"><span class="label">OneDrive Usage</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.OneDriveUsage))</span></div>
                <div class="detail-row"><span class="label">Teams Last Activity</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.TeamsActivity))</span></div>
                <div class="detail-row"><span class="label">SharePoint Sites</span><span class="value">$($m365.SharePointSites)</span></div>
                <div class="detail-row"><span class="label">MFA Status</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.MFAStatus))</span></div>
            </div>
            <h3>License Assignments</h3>
            <ul class="item-list">$licenses</ul>
            <h3>MFA Methods</h3>
            <ul class="item-list">$mfaMethods</ul>
            <h3>Conditional Access Policies</h3>
            <ul class="item-list">$caPolicies</ul>
            <h3>Shared Mailbox Access</h3>
            <ul class="item-list">$sharedMbx</ul>
        </div>
"@

    }
    else {
        '<div class="card"><h2>Microsoft 365 Details</h2><p class="unavailable">M365 data unavailable. Ensure Microsoft.Graph modules are installed and connected.</p></div>'
    }

    # Build devices section
    $devicesSection = if ($devices -and $devices.Count -gt 0) {
        $deviceRows = foreach ($d in $devices) {
            $complianceBadge = switch ($d.ComplianceState) {
                'Compliant'    { '<span class="badge badge-good">Compliant</span>' }
                'NonCompliant' { '<span class="badge badge-bad">Non-Compliant</span>' }
                default        { '<span class="badge badge-warn">' + $([System.Web.HttpUtility]::HtmlEncode($d.ComplianceState)) + '</span>' }
            }
            $lastCheck = if ($d.LastCheckIn) { $d.LastCheckIn.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' }
            $enrolled  = if ($d.EnrollmentDate) { $d.EnrollmentDate.ToString('yyyy-MM-dd') } else { 'N/A' }
            @"
            <tr>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.DeviceName))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.OS)) $([System.Web.HttpUtility]::HtmlEncode($d.OSVersion))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.Manufacturer)) $([System.Web.HttpUtility]::HtmlEncode($d.Model))</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.SerialNumber))</td>
                <td>$complianceBadge</td>
                <td>$lastCheck</td>
                <td>$enrolled</td>
                <td>$($d.IsManaged)</td>
                <td>$([System.Web.HttpUtility]::HtmlEncode($d.EncryptionStatus))</td>
            </tr>
"@

        }

        @"
        <div class="card">
            <h2>Devices ($($devices.Count))</h2>
            <div class="table-wrap">
                <table>
                    <thead>
                        <tr>
                            <th>Device Name</th><th>OS</th><th>Make / Model</th><th>Serial</th>
                            <th>Compliance</th><th>Last Check-In</th><th>Enrolled</th><th>Managed</th><th>Encryption</th>
                        </tr>
                    </thead>
                    <tbody>
                        $($deviceRows -join "`n")
                    </tbody>
                </table>
            </div>
        </div>
"@

    }
    else {
        '<div class="card"><h2>Devices</h2><p class="unavailable">No devices found or Intune data unavailable.</p></div>'
    }

    # Build sign-in history section
    $signInSection = if ($signIns) {
        $lastSuccess = if ($signIns.LastSuccessfulSignIn) { $signIns.LastSuccessfulSignIn.ToString('yyyy-MM-dd HH:mm') } else { 'None' }
        $lastFailed  = if ($signIns.LastFailedSignIn) { $signIns.LastFailedSignIn.ToString('yyyy-MM-dd HH:mm') } else { 'None' }

        $signInRows = if ($signIns.RecentSignIns -and $signIns.RecentSignIns.Count -gt 0) {
            foreach ($s in $signIns.RecentSignIns) {
                $statusBadge = if ($s.Status -eq 'Success') {
                    '<span class="badge badge-good">Success</span>'
                } else {
                    '<span class="badge badge-bad">' + $([System.Web.HttpUtility]::HtmlEncode($s.Status)) + '</span>'
                }
                $dt = if ($s.Date) { $s.Date.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' }
                @"
                <tr>
                    <td>$dt</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($s.App))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($s.IPAddress))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($s.Location))</td>
                    <td>$statusBadge</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($s.MFAResult))</td>
                </tr>
"@

            }
        }

        $riskRows = if ($signIns.RiskDetections -and $signIns.RiskDetections.Count -gt 0) {
            foreach ($r in $signIns.RiskDetections) {
                $riskDate = if ($r.DetectedDateTime) { $r.DetectedDateTime.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' }
                @"
                <tr>
                    <td>$riskDate</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskType))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskLevel))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskState))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($r.IPAddress))</td>
                    <td>$([System.Web.HttpUtility]::HtmlEncode($r.Location))</td>
                </tr>
"@

            }
        }

        $unusualHtml = if ($signIns.UnusualLocations -and $signIns.UnusualLocations.Count -gt 0) {
            $items = ($signIns.UnusualLocations | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n"
            "<h3>Unusual Locations</h3><ul class='item-list'>$items</ul>"
        } else { '' }

        @"
        <div class="card">
            <h2>Sign-In History</h2>
            <div class="detail-grid">
                <div class="detail-row"><span class="label">Last Successful Sign-In</span><span class="value">$lastSuccess</span></div>
                <div class="detail-row"><span class="label">Last Failed Sign-In</span><span class="value">$lastFailed</span></div>
                <div class="detail-row"><span class="label">Failed Attempts (period)</span><span class="value">$($signIns.FailedAttempts.Count)</span></div>
            </div>
            $(if ($signInRows) {
            @"
            <h3>Recent Sign-Ins</h3>
            <div class="table-wrap">
                <table>
                    <thead><tr><th>Date</th><th>Application</th><th>IP</th><th>Location</th><th>Status</th><th>MFA</th></tr></thead>
                    <tbody>$($signInRows -join "`n")</tbody>
                </table>
            </div>
"@
            })
            $(if ($riskRows) {
            @"
            <h3>Risk Detections</h3>
            <div class="table-wrap">
                <table>
                    <thead><tr><th>Date</th><th>Risk Type</th><th>Level</th><th>State</th><th>IP</th><th>Location</th></tr></thead>
                    <tbody>$($riskRows -join "`n")</tbody>
                </table>
            </div>
"@
            })
            $unusualHtml
        </div>
"@

    }
    else {
        '<div class="card"><h2>Sign-In History</h2><p class="unavailable">Sign-in data was not requested. Use -IncludeSignInHistory to include this section.</p></div>'
    }

    $reportDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
    $initials   = if ($displayName -and $displayName -match '(\S)\S*\s+(\S)') { ($Matches[1] + $Matches[2]).ToUpper() } else { '??' }

    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Lookup: $([System.Web.HttpUtility]::HtmlEncode($displayName))</title>
    <style>
        :root {
            --accent: #f0883e;
            --accent-dim: rgba(240, 136, 62, 0.15);
            --bg: #0d1117;
            --surface: #161b22;
            --surface-hover: #1c2129;
            --border: #30363d;
            --text: #e6edf3;
            --text-muted: #8b949e;
            --good: #3fb950;
            --bad: #f85149;
            --warn: #d29922;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
            background: var(--bg);
            color: var(--text);
            line-height: 1.6;
            padding: 2rem;
        }
        .header {
            display: flex;
            align-items: center;
            gap: 1.5rem;
            margin-bottom: 2rem;
            padding-bottom: 1.5rem;
            border-bottom: 1px solid var(--border);
        }
        .avatar {
            width: 80px; height: 80px;
            border-radius: 50%;
            background: var(--accent-dim);
            border: 2px solid var(--accent);
            display: flex; align-items: center; justify-content: center;
            font-size: 1.8rem; font-weight: 700;
            color: var(--accent);
            flex-shrink: 0;
        }
        .header-info h1 { font-size: 1.8rem; font-weight: 600; color: var(--text); }
        .header-info p { color: var(--text-muted); font-size: 0.95rem; }
        .header-info .meta { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.5rem; }
        .badge {
            display: inline-block;
            padding: 2px 10px;
            border-radius: 12px;
            font-size: 0.8rem;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.03em;
        }
        .badge-good { background: rgba(63,185,80,0.15); color: var(--good); border: 1px solid rgba(63,185,80,0.3); }
        .badge-bad { background: rgba(248,81,73,0.15); color: var(--bad); border: 1px solid rgba(248,81,73,0.3); }
        .badge-warn { background: rgba(210,153,34,0.15); color: var(--warn); border: 1px solid rgba(210,153,34,0.3); }
        .card {
            background: var(--surface);
            border: 1px solid var(--border);
            border-radius: 8px;
            padding: 1.5rem;
            margin-bottom: 1.5rem;
        }
        .card h2 {
            font-size: 1.2rem;
            color: var(--accent);
            margin-bottom: 1rem;
            padding-bottom: 0.5rem;
            border-bottom: 1px solid var(--border);
        }
        .card h3 {
            font-size: 1rem;
            color: var(--text-muted);
            margin: 1rem 0 0.5rem 0;
        }
        .detail-grid { display: grid; grid-template-columns: 1fr; gap: 0; }
        .detail-row {
            display: flex;
            justify-content: space-between;
            padding: 6px 0;
            border-bottom: 1px solid rgba(48,54,61,0.5);
        }
        .detail-row:last-child { border-bottom: none; }
        .label { color: var(--text-muted); font-size: 0.9rem; min-width: 200px; }
        .value { color: var(--text); font-size: 0.9rem; text-align: right; word-break: break-all; }
        .item-list { list-style: none; padding: 0; }
        .item-list li {
            padding: 4px 8px;
            margin: 2px 0;
            background: var(--bg);
            border-radius: 4px;
            font-size: 0.85rem;
            color: var(--text-muted);
        }
        .table-wrap { overflow-x: auto; }
        table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
        th {
            text-align: left;
            padding: 8px 12px;
            background: var(--bg);
            color: var(--text-muted);
            font-weight: 600;
            border-bottom: 2px solid var(--border);
            white-space: nowrap;
        }
        td {
            padding: 8px 12px;
            border-bottom: 1px solid var(--border);
            color: var(--text);
        }
        tr:hover td { background: var(--surface-hover); }
        .unavailable {
            color: var(--text-muted);
            font-style: italic;
            padding: 1rem 0;
        }
        .footer {
            margin-top: 2rem;
            padding-top: 1rem;
            border-top: 1px solid var(--border);
            color: var(--text-muted);
            font-size: 0.8rem;
            text-align: center;
        }
        @media (max-width: 768px) {
            body { padding: 1rem; }
            .detail-row { flex-direction: column; }
            .value { text-align: left; }
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="avatar">$initials</div>
        <div class="header-info">
            <h1>$([System.Web.HttpUtility]::HtmlEncode($displayName))</h1>
            <p>$([System.Web.HttpUtility]::HtmlEncode($title)) &mdash; $([System.Web.HttpUtility]::HtmlEncode($department))</p>
            <p>Manager: $([System.Web.HttpUtility]::HtmlEncode($manager)) &bull; Office: $([System.Web.HttpUtility]::HtmlEncode($office))</p>
            <p>$([System.Web.HttpUtility]::HtmlEncode($email))</p>
            <div class="meta">
                $enabledBadge $lockedBadge $licenseBadge $mfaBadge
            </div>
        </div>
    </div>
 
    $adSection
    $m365Section
    $devicesSection
    $signInSection
 
    <div class="footer">
        Generated by Admin-UserLookup v1.0.0 on $reportDate
    </div>
</body>
</html>
"@


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

    $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
    Write-Verbose "HTML dashboard saved to $OutputPath"
    Get-Item -Path $OutputPath
}