Private/Show-AACReportView.ps1

function Show-AACReportView {
    <#
    .SYNOPSIS
        Draws a report model at the console - the same model the HTML and
        PDF reports are written from - so the expert commands' views all look
        alike.
    .DESCRIPTION
        The model (a hashtable):
          Facts ordered: label -> value, the grey line under the title
          Status Success, Warning or Failed - the headline's state
          Headline one line: what matters most
          Tiles @{ Value; Label; Tone } (tone: good, bad, warn, info,
                     neutral, violet), drawn as Show-AACTileRow tiles
          Notices @{ Status; Text } status lines
          Charts @{ Title; Items = @{ Label; Value }; Console = $true } -
                     those marked Console are drawn as bar charts
          HeatMaps @{ Title; RowHeader; Columns; Rows = @{ Label; Group;
                     Cells = @(@{ Score } or $null); Total }; ConsoleColumns (8) } -
                     rows by columns of 0-100 scores, each cell coloured by
                     its score (the first ConsoleColumns columns, and All)
          Tables @{ Title; Rows; Columns; ConsoleLimit (15) } - the
                     columns marked Console (else the first six shown) of
                     the first ConsoleLimit rows; badge columns in their
                     tone's colour, severities with their status symbol
          Hint a grey line at the end (the parameters to go further)
        Tables with no rows are left out, unless they say Empty (a callout).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Report
    )

    $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) }
    $glyph = Get-AACGlyph
    $toneColor = @{ good = 'green3'; bad = 'red1'; warn = 'orange1'; info = 'deepskyblue1'; neutral = 'grey70'; violet = 'mediumpurple2' }
    $severity = Get-AACSeverityRank
    $has = { param([string] $Key) $Report.Contains($Key) -and $null -ne $Report[$Key] }

    $facts = [System.Collections.Generic.List[string]]::new()
    if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") }
    if (& $has 'Facts') { foreach ($key in $Report.Facts.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Report.Facts[$key])") } }
    $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm'))
    Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]"
    [Spectre.Console.AnsiConsole]::WriteLine()
    if (& $has 'Headline') {
        Write-AACStatusLine $(if (& $has 'Status') { $Report.Status } else { 'Info' }) ([string]$Report.Headline)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    if ((& $has 'Tiles') -and @($Report.Tiles).Count) {
        Show-AACTileRow -Tile @(foreach ($t in $Report.Tiles) { @{ Value = [string]$t.Value; Caption = [string]$t.Label; Color = $(if ($t.Contains('Tone') -and $toneColor.Contains([string]$t.Tone)) { $toneColor[[string]$t.Tone] } else { 'deepskyblue1' }) } })
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    if (& $has 'Notices') {
        foreach ($n in @($Report.Notices)) { Write-AACStatusLine $(if ($n.Contains('Status')) { $n.Status } else { 'Warning' }) ([string]$n.Text) }
        if (@($Report.Notices).Count) { [Spectre.Console.AnsiConsole]::WriteLine() }
    }
    if (& $has 'Charts') {
        foreach ($c in @($Report.Charts | Where-Object { $_.Contains('Console') -and $_.Console -and @($_.Items).Count })) {
            Show-AACBarChart -Item @(foreach ($i in @($c.Items) | Select-Object -First 12) { @{ Label = [string]$i.Label; Value = [double]$i.Value } }) -Title ([string]$c.Title)
            [Spectre.Console.AnsiConsole]::WriteLine()
        }
    }

    # Heat maps: a cell's background is its score, as in an Azure workbook.
    $heat = {
        param($Cell)
        if ($null -eq $Cell -or $null -eq $Cell.Score) { return '[grey42]-[/]' }
        $s = [double]$Cell.Score
        $style = if ($s -ge 90) { 'black on green3' } elseif ($s -ge 75) { 'black on darkseagreen2' } elseif ($s -ge 50) { 'black on gold1' } elseif ($s -ge 25) { 'black on orange1' } else { 'white on red3' }
        "[$style] $('{0,3:N0}' -f $s) [/]"
    }
    foreach ($h in @(if (& $has 'HeatMaps') { $Report.HeatMaps })) {
        $limit = if ($h.Contains('ConsoleColumns')) { [int]$h.ConsoleColumns } else { 8 }
        $shown = @($h.Columns | Select-Object -First $limit)
        $table = [Spectre.Console.Table]::new()
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderStyle = [Spectre.Console.Style]::Parse('grey35')
        $table.Title = [Spectre.Console.TableTitle]::new("[bold]$(& $escape $h.Title)[/]$(if (@($h.Columns).Count -gt $limit) { " [grey58](first $limit of $(@($h.Columns).Count) columns; the HTML and PDF reports have them all)[/]" })")
        $null = $table.AddColumn([Spectre.Console.TableColumn]::new("[grey70]$(& $escape $(if ($h.Contains('RowHeader')) { $h.RowHeader } else { '' }))[/]"))
        foreach ($c in $shown + 'All') {
            $column = [Spectre.Console.TableColumn]::new("[grey70]$(& $escape $c)[/]")
            $column.Alignment = [Spectre.Console.Justify]::Center
            $null = $table.AddColumn($column)
        }
        $group = $null
        foreach ($r in @($h.Rows)) {
            if ($r.Contains('Group') -and $r.Group -and $r.Group -ne $group) {
                $group = $r.Group
                $null = [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]@(@("[bold]$(& $escape $group)[/]") + @($shown | ForEach-Object { '' }) + '' | ForEach-Object { [Spectre.Console.Markup]::new($_) }))
            }
            $cells = @(for ($i = 0; $i -lt $shown.Count; $i++) { & $heat @($r.Cells)[$i] })
            $markup = @(" $(& $escape $r.Label)") + $cells + @(& $heat $(if ($r.Contains('Total')) { $r.Total }))
            $null = [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]@($markup | ForEach-Object { [Spectre.Console.Markup]::new($_) }))
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        Write-AACMarkup '[grey50]Score out of 100:[/] [white on red3] <25 [/] [black on orange1] 25-49 [/] [black on gold1] 50-74 [/] [black on darkseagreen2] 75-89 [/] [black on green3] 90+ [/] [grey42]- not assessed[/]'
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    foreach ($t in @(if (& $has 'Tables') { $Report.Tables })) {
        $rows = @($t.Rows | Where-Object { $null -ne $_ })
        if (-not $rows.Count) {
            if ($t.Contains('Empty') -and $t.Empty) { Show-AACCallout $(if ($t.Contains('EmptyStatus')) { $t.EmptyStatus } else { 'Success' }) "[bold]$(& $escape $t.Empty)[/]" -Title ([string]$t.Title); [Spectre.Console.AnsiConsole]::WriteLine() }
            continue
        }
        if ($t.Contains('NoConsole') -and $t.NoConsole) { continue }
        $columns = @($t.Columns | Where-Object { -not ($_.Contains('Hidden') -and $_.Hidden) })
        $chosen = @($columns | Where-Object { $_.Contains('Console') -and $_.Console })
        if (-not $chosen.Count) { $chosen = @($columns | Select-Object -First 6) }
        $limit = if ($t.Contains('ConsoleLimit')) { [int]$t.ConsoleLimit } else { 15 }
        $table = [Spectre.Console.Table]::new()
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderStyle = [Spectre.Console.Style]::Parse('grey35')
        $title = "[bold]$(& $escape $t.Title)[/]$(if ($rows.Count -gt $limit) { " [grey58](top $limit of $($rows.Count))[/]" } else { " [grey58]($($rows.Count))[/]" })"
        $table.Title = [Spectre.Console.TableTitle]::new($title)
        foreach ($c in $chosen) {
            $column = [Spectre.Console.TableColumn]::new("[grey70]$(& $escape $c.Label)[/]")
            if ($c.Contains('Type') -and $c.Type -in 'number', 'money', 'score') { $column.Alignment = [Spectre.Console.Justify]::Right }
            $null = $table.AddColumn($column)
        }
        foreach ($row in @($rows | Select-Object -First $limit)) {
            $cells = foreach ($c in $chosen) {
                $value = if ($row -is [System.Collections.IDictionary]) { $row[$c.Key] } else { $p = $row.PSObject.Properties[[string]$c.Key]; if ($p) { $p.Value } }
                $text = if ($value -is [datetime]) { $value.ToString('d MMM HH:mm') } elseif ($value -is [double] -or $value -is [decimal]) { if ($c.Contains('Format')) { $value.ToString($c.Format) } else { '{0:N2}' -f $value } } elseif ($value -is [int] -or $value -is [long]) { '{0:N0}' -f $value } else { [string]$value }
                $type = if ($c.Contains('Type')) { $c.Type } else { 'text' }
                if ($c.Key -eq 'Severity' -and $severity.Status.Contains($text)) {
                    "$((Get-AACStatus $severity.Status[$text]).Markup) [$($toneColor[$severity.Tone[$text]])]$(& $escape $text)[/]"
                }
                elseif ($c.Key -eq 'Status' -and $text -in 'Success', 'Warning', 'Failed', 'Info', 'InProgress') {
                    $state = Get-AACStatus $text
                    "$($state.Markup) [$($state.Color)]$(& $escape $text)[/]"
                }
                elseif ($type -eq 'badge' -and $c.Contains('Tones') -and $c.Tones -and $c.Tones.Contains($text) -and $toneColor.Contains([string]$c.Tones[$text])) {
                    "[$($toneColor[[string]$c.Tones[$text]])]$(& $escape $text)[/]"
                }
                elseif ($type -eq 'resource') { "[white]$(& $escape $text)[/]" }
                elseif ($type -in 'mono', 'link') { "[grey58]$(& $escape $text)[/]" }
                elseif ($type -eq 'wide') { "[grey70]$(& $escape $text)[/]" }
                else { & $escape $text }
            }
            $null = [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]@($cells | ForEach-Object { [Spectre.Console.Markup]::new($_) }))
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    if (& $has 'Hint') { Write-AACMarkup "[grey42]$(& $escape $Report.Hint)[/]" }
}