Private/New-HtmlDashboard.ps1
|
function New-HtmlDashboard { <# .SYNOPSIS Generates a dark-themed HTML dashboard report with teal accent (#2dd4bf). .DESCRIPTION Creates styled HTML reports for CI history, user history, recurring issues, and knowledge gap analysis. Each report type has its own layout optimized for the data being presented. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('CIHistory', 'UserHistory', 'RecurringIssues', 'KnowledgeGaps')] [string]$ReportType, [Parameter(Mandatory)] [object]$Data, [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [string]$Title ) # Base CSS for all reports $baseCss = @' * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; padding: 2rem; } .container { max-width: 1200px; margin: 0 auto; } h1 { color: #2dd4bf; font-size: 1.8rem; margin-bottom: 0.5rem; border-bottom: 2px solid #2dd4bf; padding-bottom: 0.5rem; } h2 { color: #2dd4bf; font-size: 1.3rem; margin: 1.5rem 0 0.75rem 0; } h3 { color: #94a3b8; font-size: 1.1rem; margin: 1rem 0 0.5rem 0; } .subtitle { color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem; } .card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; } .card-accent { border-left: 4px solid #2dd4bf; } .card-warning { border-left: 4px solid #f59e0b; } .card-danger { border-left: 4px solid #ef4444; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0; } .stat-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center; } .stat-value { font-size: 2rem; font-weight: 700; color: #2dd4bf; } .stat-label { color: #94a3b8; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; } .summary-box { background: #1e293b; border: 1px solid #2dd4bf; border-radius: 8px; padding: 1.5rem; margin: 1rem 0; white-space: pre-wrap; font-size: 0.95rem; } table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; } th { background: #334155; color: #2dd4bf; padding: 0.75rem; text-align: left; font-weight: 600; position: sticky; top: 0; } td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #334155; } tr:hover { background: #1e293b; } .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 600; } .badge-incident { background: #7f1d1d; color: #fca5a5; } .badge-change { background: #1e3a5f; color: #93c5fd; } .badge-problem { background: #713f12; color: #fde68a; } .badge-request { background: #14532d; color: #86efac; } .badge-open { background: #7f1d1d; color: #fca5a5; } .badge-closed { background: #14532d; color: #86efac; } .badge-resolved { background: #1e3a5f; color: #93c5fd; } .badge-missing { background: #7f1d1d; color: #fca5a5; } .badge-stale { background: #713f12; color: #fde68a; } .badge-incomplete { background: #1e3a5f; color: #93c5fd; } .timeline { border-left: 3px solid #2dd4bf; margin: 1rem 0 1rem 1rem; padding-left: 1.5rem; } .timeline-item { position: relative; margin-bottom: 1.25rem; } .timeline-item::before { content: ''; position: absolute; left: -1.85rem; top: 0.4rem; width: 10px; height: 10px; border-radius: 50%; background: #2dd4bf; } .timeline-date { color: #2dd4bf; font-weight: 600; font-size: 0.85rem; } .timeline-content { color: #cbd5e1; font-size: 0.9rem; } .pattern-card { background: #1e293b; border: 1px solid #334155; border-left: 4px solid #f59e0b; border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; } .pattern-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } .pattern-count { background: #f59e0b; color: #0f172a; padding: 0.2rem 0.6rem; border-radius: 12px; font-weight: 700; font-size: 0.85rem; } .footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #334155; color: #64748b; font-size: 0.8rem; text-align: center; } pre { background: #0f172a; border: 1px solid #334155; border-radius: 4px; padding: 1rem; overflow-x: auto; white-space: pre-wrap; font-size: 0.85rem; color: #cbd5e1; } '@ $generatedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' # Build report body based on type $bodyHtml = switch ($ReportType) { 'CIHistory' { $reportTitle = if ($Title) { $Title } else { "CI History: $($Data.CIName)" } Build-CIHistoryHtml -Data $Data -ReportTitle $reportTitle } 'UserHistory' { $reportTitle = if ($Title) { $Title } else { "User Ticket History: $($Data.UserName)" } Build-UserHistoryHtml -Data $Data -ReportTitle $reportTitle } 'RecurringIssues' { $reportTitle = if ($Title) { $Title } else { 'Recurring Issues Analysis' } Build-RecurringIssuesHtml -Data $Data -ReportTitle $reportTitle } 'KnowledgeGaps' { $reportTitle = if ($Title) { $Title } else { 'Knowledge Gap Analysis' } Build-KnowledgeGapsHtml -Data $Data -ReportTitle $reportTitle } } $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$reportTitle - ITSM Insights</title> <style> $baseCss </style> </head> <body> <div class="container"> $bodyHtml <div class="footer"> Generated by ITSM-Insights on $generatedAt | AI-powered ITSM ticket intelligence </div> </div> </body> </html> "@ # Ensure output directory exists $outputDir = Split-Path $OutputPath -Parent if ($outputDir -and -not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Verbose "HTML report saved to: $OutputPath" return $OutputPath } function Build-CIHistoryHtml { param([object]$Data, [string]$ReportTitle) $statsHtml = @" <h1>$ReportTitle</h1> <p class="subtitle">Ticket history analysis covering $($Data.TicketCount) tickets</p> <div class="stats-grid"> <div class="stat-card"> <div class="stat-value">$($Data.TicketCount)</div> <div class="stat-label">Total Tickets</div> </div> <div class="stat-card"> <div class="stat-value">$($Data.IncidentCount)</div> <div class="stat-label">Incidents</div> </div> <div class="stat-card"> <div class="stat-value">$($Data.ChangeCount)</div> <div class="stat-label">Changes</div> </div> <div class="stat-card"> <div class="stat-value">$($Data.ProblemCount)</div> <div class="stat-label">Problems</div> </div> </div> "@ # AI Summary $summaryHtml = '' if ($Data.Summary) { $escapedSummary = [System.Net.WebUtility]::HtmlEncode($Data.Summary) $summaryHtml = @" <h2>AI Analysis Summary</h2> <div class="summary-box">$escapedSummary</div> "@ } # Timeline $timelineHtml = '' if ($Data.Timeline -and $Data.Timeline.Count -gt 0) { $timelineItems = foreach ($item in $Data.Timeline) { $badgeClass = switch -Wildcard ($item.Type) { '*Incident*' { 'badge-incident' } '*Change*' { 'badge-change' } '*Problem*' { 'badge-problem' } default { 'badge-request' } } @" <div class="timeline-item"> <div class="timeline-date">$($item.Date) <span class="badge $badgeClass">$($item.Type)</span></div> <div class="timeline-content"><strong>$($item.Number)</strong> - $([System.Net.WebUtility]::HtmlEncode($item.Description))</div> </div> "@ } $timelineHtml = @" <h2>Event Timeline</h2> <div class="timeline"> $($timelineItems -join "`n") </div> "@ } # Open Items $openItemsHtml = '' if ($Data.OpenItems -and $Data.OpenItems.Count -gt 0) { $openRows = foreach ($item in $Data.OpenItems) { "<tr><td>$($item.Number)</td><td>$([System.Net.WebUtility]::HtmlEncode($item.ShortDescription))</td><td>$($item.Priority)</td><td>$($item.AssignedTo)</td></tr>" } $openItemsHtml = @" <h2>Open Items</h2> <div class="card card-warning"> <table> <tr><th>Number</th><th>Description</th><th>Priority</th><th>Assigned To</th></tr> $($openRows -join "`n") </table> </div> "@ } # Ticket table $ticketRows = foreach ($ticket in ($Data.RawTickets | Select-Object -First 50)) { $badgeClass = switch -Wildcard ($ticket.Type) { '*Incident*' { 'badge-incident' } '*Change*' { 'badge-change' } '*Problem*' { 'badge-problem' } default { 'badge-request' } } $stateClass = switch -Wildcard ($ticket.State) { '*Open*' { 'badge-open' } '*New*' { 'badge-open' } '*Progress*' { 'badge-open' } '*Closed*' { 'badge-closed' } '*Resolved*' { 'badge-resolved' } default { 'badge-resolved' } } "<tr><td>$($ticket.Number)</td><td><span class=`"badge $badgeClass`">$($ticket.Type)</span></td><td>$([System.Net.WebUtility]::HtmlEncode($ticket.ShortDescription))</td><td><span class=`"badge $stateClass`">$($ticket.State)</span></td><td>$($ticket.Priority)</td><td>$($ticket.OpenedAt)</td></tr>" } $ticketTableHtml = @" <h2>All Tickets</h2> <table> <tr><th>Number</th><th>Type</th><th>Description</th><th>State</th><th>Priority</th><th>Opened</th></tr> $($ticketRows -join "`n") </table> "@ return "$statsHtml`n$summaryHtml`n$timelineHtml`n$openItemsHtml`n$ticketTableHtml" } function Build-UserHistoryHtml { param([object]$Data, [string]$ReportTitle) $totalTickets = 0 $roleBreakdown = '' if ($Data.TicketsByRole) { foreach ($role in $Data.TicketsByRole.PSObject.Properties) { $count = @($role.Value).Count $totalTickets += $count $roleBreakdown += " <div class=`"stat-card`"><div class=`"stat-value`">$count</div><div class=`"stat-label`">As $($role.Name)</div></div>`n" } } $html = @" <h1>$ReportTitle</h1> <p class="subtitle">Complete ticket interaction history</p> <div class="stats-grid"> <div class="stat-card"> <div class="stat-value">$totalTickets</div> <div class="stat-label">Total Tickets</div> </div> $roleBreakdown </div> "@ if ($Data.Summary) { $escapedSummary = [System.Net.WebUtility]::HtmlEncode($Data.Summary) $html += @" <h2>AI Analysis</h2> <div class="summary-box">$escapedSummary</div> "@ } if ($Data.CommonIssues -and $Data.CommonIssues.Count -gt 0) { $issueCards = foreach ($issue in $Data.CommonIssues) { "<div class=`"card card-accent`"><strong>$([System.Net.WebUtility]::HtmlEncode($issue))</strong></div>" } $html += @" <h2>Common Issue Types</h2> $($issueCards -join "`n") "@ } if ($Data.OpenItems -and $Data.OpenItems.Count -gt 0) { $openRows = foreach ($item in $Data.OpenItems) { "<tr><td>$($item.Number)</td><td>$([System.Net.WebUtility]::HtmlEncode($item.ShortDescription))</td><td>$($item.State)</td></tr>" } $html += @" <h2>Open Items</h2> <div class="card card-warning"> <table> <tr><th>Number</th><th>Description</th><th>State</th></tr> $($openRows -join "`n") </table> </div> "@ } return $html } function Build-RecurringIssuesHtml { param([object]$Data, [string]$ReportTitle) $html = @" <h1>$ReportTitle</h1> <p class="subtitle">AI-detected patterns across ticket history</p> <div class="stats-grid"> <div class="stat-card"> <div class="stat-value">$(@($Data).Count)</div> <div class="stat-label">Patterns Detected</div> </div> <div class="stat-card"> <div class="stat-value">$(($Data | Measure-Object -Property Occurrences -Sum).Sum)</div> <div class="stat-label">Total Occurrences</div> </div> </div> "@ foreach ($pattern in $Data) { $ticketList = if ($pattern.TicketNumbers) { ($pattern.TicketNumbers -join ', ') } else { 'N/A' } $escapedResolution = [System.Net.WebUtility]::HtmlEncode($pattern.SuggestedResolution) $html += @" <div class="pattern-card"> <div class="pattern-header"> <h3>$([System.Net.WebUtility]::HtmlEncode($pattern.Pattern))</h3> <span class="pattern-count">$($pattern.Occurrences) occurrences</span> </div> <p><strong>Tickets:</strong> $ticketList</p> <p><strong>First Seen:</strong> $($pattern.FirstSeen) | <strong>Last Seen:</strong> $($pattern.LastSeen)</p> <p><strong>Estimated Time Saved if Fixed:</strong> $($pattern.EstimatedTimeSaved)</p> <h3>Suggested Resolution</h3> <pre>$escapedResolution</pre> </div> "@ } return $html } function Build-KnowledgeGapsHtml { param([object]$Data, [string]$ReportTitle) $html = @" <h1>$ReportTitle</h1> <p class="subtitle">Knowledge base gaps identified from ticket analysis</p> <div class="stats-grid"> <div class="stat-card"> <div class="stat-value">$(@($Data | Where-Object { $_.GapType -eq 'Missing' }).Count)</div> <div class="stat-label">Missing Articles</div> </div> <div class="stat-card"> <div class="stat-value">$(@($Data | Where-Object { $_.GapType -eq 'Stale' }).Count)</div> <div class="stat-label">Stale Articles</div> </div> <div class="stat-card"> <div class="stat-value">$(@($Data | Where-Object { $_.GapType -eq 'Incomplete' }).Count)</div> <div class="stat-label">Incomplete Articles</div> </div> </div> "@ foreach ($gap in $Data) { $badgeClass = switch ($gap.GapType) { 'Missing' { 'badge-missing' } 'Stale' { 'badge-stale' } 'Incomplete' { 'badge-incomplete' } default { 'badge-missing' } } $ticketList = if ($gap.RelatedTickets) { ($gap.RelatedTickets -join ', ') } else { 'N/A' } $escapedContent = [System.Net.WebUtility]::HtmlEncode($gap.SuggestedContent) $html += @" <div class="card card-accent"> <div class="pattern-header"> <h3>$([System.Net.WebUtility]::HtmlEncode($gap.Topic))</h3> <span class="badge $badgeClass">$($gap.GapType)</span> </div> <p><strong>Suggested Title:</strong> $([System.Net.WebUtility]::HtmlEncode($gap.SuggestedTitle))</p> <p><strong>Related Tickets:</strong> $ticketList</p> <h3>Suggested Content</h3> <pre>$escapedContent</pre> </div> "@ } return $html } |