wks_htmlReporting.psm1

function format-htmlTreeLevel() {
    [cmdletbinding()]
    param(
        $dataTree, 
        $idField, 
        $parentidField, 
        $nameField, 
        $parentIdValue, 
        $method,
        $progressId,
        $progressCountVariable,
        $progressTotal
    )
    if ($progressCountVariable) {
        $progress = get-variable -name $progressCountVariable -valueonly -scope global
        write-progress -id $progressId -activity "Building TreeView" -PercentComplete ($progress*100/$progressTotal)
        $progress++
        set-variable -name $progressCountVariable -value $progress -force -scope global
    }

    $item = $dataTree |? { $_.$idField -eq $parentIdValue}
    $iconColorStr = ""
    if ($null -ne $item.icon_color) { $iconColorStr = "style='color:$($item.icon_color)'" }
    
    $childItems = @($dataTree |? { $_.$parentidField -eq $parentIdValue })
    if ($childItems.count -gt 0) {
        #write-verbose "Nested Node $($item.$nameField)"
       "<li class='treeview-animated-items'>
        <a id='$($item.$idField)' onClick='$method(this.id)' class='closed'>
            <i class='fas fa-angle-right'></i>
            <span><i class='far ic-w mx-1 $($item.icon)' $iconColorStr></i>$($item.$nameField)</span>
        </a>
        <ul class='nested'>"

        foreach ($childitem in $childItems) {
            if ($progressCountVariable) {
                format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField `
                    -nameField $nameField -parentidValue $childitem.$idField -method $method `
                    -progressId $progressId -progressCountVariable $progressCountVariable -progressTotal $progressTotal
            } else {
                format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField `
                    -nameField $nameField -parentidValue $childitem.$idField -method $method
            }
        }
        "</ul>
        </li>"


    } else {
        #write-verbose "End nesting $($item.$nameField)"
        "<li>
        <div id='$($item.$idField)' onClick='$method(this.id)' class='treeview-animated-element'><i class='far ic-w mr-1 $($item.icon)' $iconColorStr></i>$($item.$nameField)
        </li>"

    }

    
}
function get-randomId {
    param(
        $length = 8
    )
    # Characters
    $chars = (48..57) + (65..90) + (97..122)
    [string]$Result = $null
    # First char cannot be numeric (causes issues with Javascript for DOM node id reference)
    (65..90) + (97..122) | Get-Random |% { $Result += [char]$_ }
    $chars | Get-Random -Count ($length -1) | ForEach-Object{ $Result += [char]$_ }
    $Result
}

function get-translatedNames {
    param(
        $collection,
        [string[]]$ids,
        $idKey='id',
        $valueKey='displayName',
        [switch]$html
    )

    $result = @()
    foreach ($id in $ids) {
        $obj = $collection |? {$_.$idKey -eq $id}
        if ($null -ne $obj) { $result += ("$($obj.$valueKey)"  -replace ' ','&nbsp;') } else { $result += $id}
    }
    
    if ($html) { $result -join "<br/>" }
    else { $result -join ', ' }
}


function get-htmlGroupedProperties {
    [cmdletbinding()]
    param 
    (
        [string[]]$attributes,
        [pscustomobject]$object,
        [switch]$showTitle,
        [hashtable]$attributeMappingTable,
        [hashtable]$collapsableAttributes,
        [hashtable]$collapsableAttributesMaxValue
    )

    if ($object) {
        foreach ($attrib in $attributes) {
            if ($null -ne $object.$attrib) {
                # Apply key name mapping
                $key = $attrib
                if ($attributeMappingTable) {
                    if ($null -ne $attributeMappingTable[$attrib]) {
                        $key = $attributeMappingTable[$attrib]
                    }
                }
                # by default : concatenate values
                $val = $(($object.$attrib -replace ' ','&nbsp;') -join '<br/>')
                
                if ($attrib -like "*user*") {
                    $val = get-translatedNames -collection $users -ids $object.$attrib -html
                    
                }
                if ($attrib -like "*group*") {
                    $val = get-translatedNames -collection $groups -ids $object.$attrib -html
                }
                if ($attrib -like "*application*") {
                    $val = get-translatedNames -collection $applications -ids $object.$attrib -idKey 'appId' -html
                }
                if ($attrib -like "signInFrequency") {
                    $val = "$($object.$attrib.value) $($object.$attrib.type) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))"
                }
                if ($attrib -like "persistentBrowser") {
                    $val = "$($object.$attrib.mode) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))"
                }
                if ($attrib -like "cloudAppSecurity") {
                    $val = "$($object.$attrib.cloudAppSecurityType) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))"
                }
                if ($attrib -like "*Roles*") {
                    $val = get-translatedNames -collection $roles -ids $object.$attrib -html
                }
                if ($attrib -like "termsOfUse") {
                    
                    $val = @($object.$attrib |% { $reportConfig.aadTermsOfUseMapping[$_] }) -join ', '
                }

                # builtin controls
                if ($attrib -like "builtInControls") {
                    $val = $($object.$attrib -join " $($object.operator) ")
                }
                if ($attrib -like "operator") {
                    $val = ''
                }
                
                Write-Verbose "Processing attribute $attrib"
                if ($val -ne '') { 
                    if ($showTitle) {
                        $itemCount = @($object.$attrib).count
                        Write-verbose "- itemCount = $itemCount"
                        $htmlBadge = ''
                        $collapsing = ''
                        $linkId = ''
                        Write-Verbose "- collapsableAttributes : $($collapsableAttributes.keys -join ',')"
                        $collapsableTreshold = $null
                        if ($collapsableAttributes.keys -contains $attrib) {
                            Write-Verbose "- collapsableAttributes contains attrib '$attrib'"
                            $collapsableTreshold = $collapsableAttributes[$attrib]
                        }
                        if ($collapsableAttributes.keys -contains '*') {
                            Write-Verbose "- collapsableAttributes contains attrib '*'"
                            $collapsableTreshold = $collapsableAttributes['*']
                        }
                        Write-verbose "- collapsableTreshold = $collapsableTreshold"
                        # Add a badge to count items (combined with item collapse)
                        if ($null -ne $collapsableTreshold) {
                            if ($itemCount -gt $collapsableTreshold) {
                                $allItems = ''
                                # if all roles are listed add the (All) information into html badge
                                foreach ($attribMax in $collapsableAttributesMaxValue.keys) {
                                    if (($attrib -like $attribMax) -and ($itemCount -eq $collapsableAttributesMaxValue[$attribMax])) { $allItems = '(All)'}
                                }
                                #
                                # Adds a badge with the item count (for lisibility when collapsing)
                                $htmlBadge = "<span class='badge badge-pill badge-primary'> $itemCount $allItems </span>" 
                                $collapsing = "collapse"
                                $linkId = get-randomId
                            }
                        }
                        $val = "
                        <div class='card'>
                            <div class='card-header mb-1'>
                                <a $(if ($linkId -ne '') {"href='#$linkId'"} )data-bs-toggle='collapse' class='card-link'>$key $htmlBadge</a>
                            </div>
                            <div id='$linkId' class='$collapsing mb-1'>
                                <div class='card-body mb-1'>$val</div>
                            </div>
                        </div>"

                    } else {
                        $val = "<p class='mb-1'>$val</p>"
                    }
                    $val
                }
            }   
        }   
    }
}

<#
#
# HTML Item
#
#>


function new-htmlItemTable {
    [cmdletbinding(DefaultParameterSetName = 'textTable')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='textTable')]
        [Parameter(Mandatory=$true, ParameterSetName='htmlTable')]
        $dataset,
        [Parameter(Mandatory=$false, ParameterSetName='textTable')]
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        [string[]]$filterKeys, 
        [Parameter(Mandatory=$false, ParameterSetName='textTable')]
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        [string[]]$hiddenKeys=@(), 
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        $notificationLevelQuery,
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        [switch]$dataContainsHtml,
        [Parameter(Mandatory=$false, ParameterSetName='textTable')]
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        [switch]$noPaging,
        [Parameter(Mandatory=$false, ParameterSetName='textTable')]
        [Parameter(Mandatory=$false, ParameterSetName='htmlTable')]
        [switch]$noSearching
    )
    $htmlItem = new-object -typename htmlItem
    
    $keys = $dataset |select-object -excludeproperty $hiddenKeys |Get-Member -MemberType noteproperty |select -ExpandProperty name -unique
    if ($null -eq $keys) { $keys = $dataset.keys |select -unique |? {$hiddenKeys -notcontains $_} }
    # filter keys to required fields (also force fileds order at display)
    if ($null -ne $filterKeys) {
        $keys = $filterKeys |? {$keys -contains $_}
    }
    
    # Having HTML in TD causes issues with search feature when data is imported from JSON
    # In this situation, we code the data as full HTML (slower and inefficient for large amount of data)
    if ($dataContainsHtml) {
        $htmlItem.htmlContent += "
        <div class='col w-20 mx-4 my-4 w-auto'>
            <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'>
            <thead class='table-primary'>
                <tr>
                    $($keys |foreach-object {
                        if ($hiddenKeys -contains $_) {
                            "<th class='visually-hidden'>$_</th>"
                        } else {
                            "<th>$_</th>"
                        }
                    })
                </tr>
            </thead>
            <tbody>"

        foreach ($item in $dataset) {
            $myClass = ''
            foreach ($query in $notificationLevelQuery.keys) {
                Write-Verbose "Item state: $($item.state)"
                Write-Verbose "- Query: $query"
                Write-Verbose "- Query result: $($item |% {iex $query})"
                if ($item |% {iex $query}) {
                    $myClass = "alert "
                    if ($notificationLevelQuery[$query] -eq 'Warning') { $myClass += "alert-warning" }
                    if ($notificationLevelQuery[$query] -eq 'Critical') { $myClass += "alert-danger" }
                    Write-Verbose "- myClass: $myClass"
                }
            }

            $htmlItem.htmlContent += "<tr class='$myClass'>$($keys |foreach-object {
                $strClass = ''
                if ($hiddenKeys -contains $_) {
                    $strClass = "class='visually-hidden'"
                }
                 
                if ($_ -eq (@($keys)|select -first 1)) {
                    "<th $strClass>$($item.$_)</th>"
                } else {
                    "<td $strClass>$($item.$_)</td>"
                }
            })</tr>"

        }
        $htmlItem.htmlContent += '</tbody>
            </table></div>'

        # Run table processing to display paging / sorting etc.
        $htmlItem.javascriptOnLoad += "`$('#$($htmlItem.id)').DataTable({
            searchHighlight: true,
            paging: $(if ($noPaging) {"false"} else {"true"}),
            searching: $(if ($noSearching) {"false"} else {"true"}),
            columnDefs: [
                {
                    targets: '_all',
                    'render': function (data, type, row, meta ) {
                        if(type === 'display'){
                            let aStr = ('' + data).split(',')
                            if (aStr.length > 1) {
                                var str = '';
                                aStr.forEach((item)=>{
                                    str += item + '<br/>';
                                });
                                return str;
                            } else {
                                return data
                            }
                        }else{
                            return data;
                        }
                    }
                }
            ]
        });"

    
    } else {
        $tableid = "table$(get-randomId)"
        $htmlItem.htmlContent += "
                        
            <div class='col w-20 mx-4 my-4 w-auto'>
                <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'>
                <thead class='table-primary'>
                    <tr>
                        $($keys |foreach-object { "<th>$_</th>" })
                    </tr>
                </thead>
                <tbody></tbody>
                </table>
            </div>"

        $mydata = $dataset |ConvertTo-Json
        if (($dataset.gettype().basetype.name -eq 'Array') -and ($dataset.count -eq 1)) { $mydata = "[$mydata]"}
        $htmlItem.javascriptOnLoad += "var jsonData = $mydata;
            `$('#$($htmlItem.id)').DataTable({
            data: jsonData,
            columns: [
                $(($keys |% { "{ data: '$_' }"}) -join ',')
            ],
            columnDefs: [
                {
                    targets: '_all',
                    'render': function (data, type, row, meta ) {
                        if(type === 'display'){
                            let aStr = ('' + data).split(',')
                            if (aStr.length > 1) {
                                var str = '';
                                aStr.forEach((item)=>{
                                    str += item + '<br/>';
                                });
                                return str;
                            } else {
                                return data
                            }
                        }else{
                            return data;
                        }
                    }
                },
                $(if ($hiddenKeys) {
                    ($hiddenKeys |% { if ($(@($keys).indexof($_) -gt -1)) { "{
                        targets: [ $(@($keys).indexof($_) ) ],
                        visible: false,
                        searchable: false
                    } "}}) -join ','
                })
            ],
            searchHighlight: true,
            paging: $(if ($noPaging) {"false"} else {"true"}),
            searching: $(if ($noSearching) {"false"} else {"true"})
        });"

    }
    return $htmlItem
}

function new-htmlItemNotifications {
    param(
        [Parameter(Mandatory)]
        [hashtable]$notificationTable
    )
    $htmlItem = new-object -typename htmlItem

    $htmlItem.htmlContent += "
    <div class='card col'>
        <div class='card-body row'>"

    foreach ($notif in $notificationTable.keys) {
        if (@("Information", "Success", "Warning", "Critical") -notcontains $notificationTable[$notif]) {
            throw "The value associated to the key '$notif' is not part of the accepted values ('Information', 'Success', 'Warning', 'Critical')"
        }
        $strLevel = "alert "
        if ($notificationTable[$notif] -eq 'Success') { $strLevel += "alert-success" }
        if ($notificationTable[$notif] -eq 'Warning') { $strLevel += "alert-warning" }
        if ($notificationTable[$notif] -eq 'Critical') { $strLevel += "alert-danger" }
        $htmlItem.htmlContent += "<div class='$strLevel'>$notif</div>"
    }
    $htmlItem.htmlContent +=  "
    </div>
    </div>"


    return $htmlItem
}


function new-htmlItemTree {
    [cmdletbinding()]
    param(
        $dataTree, 
        $idField, 
        $parentidField, 
        $nameField, 
        $detailField
    )
    $htmlItem = new-object -typename htmlItem

    $htmlItem.htmlContent += "<div class='container card-body row align-items-start'>
        <div class='col-1 treeview-animated w-20 border mx-4 my-4'>
        <ul class='treeview-animated-list mb-3'>"

    $itemsToProcess = @()
    # Start with root items
    $itemsToProcess += $dataTree |? { ($null -eq $_.$parentidField) -or ('' -eq $_.$parentidField.trim()) `
        -or ($dataTree.$idField -notcontains $_.$parentidField) }
    write-verbose "Found $($itemsToProcess.count) root items"
    foreach ($item in ($itemsToProcess |sort-object -property $nameField)) {
        Write-Verbose "Processing Node $($item.$nameField)"
        $htmlItem.htmlContent += format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField `
            -nameField $nameField -parentidValue $item.$idField -method 'showTreeDetails' -verbose
    }
    
    $htmlItem.htmlContent += "
        </ul>
        </div>
        <div id='$($htmlItem.id)' class='col-4 w-20 mx-4 my-4'>
            <!--Details-->
        </div>
 
        <script>
 
        function showTreeDetails(elementId)
        {
            var tree_$($htmlItem.id) = $($dataTree |convertto-json);
            jQuery.map(tree_$($htmlItem.id), function(obj) {
                if (obj.$($idField) === elementId) {
                    `$('#$($htmlItem.id)').text(obj.$detailField);
                }
            });
             
        }
        </script>
        </div>"

    $htmlItem.javascriptOnLoad += "`$('.treeview-animated').mdbTreeview();"
    return $htmlItem
}

function new-htmlItemTreeWithDetailedTable {
    [cmdletbinding(DefaultParameterSetName = 'simple')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        $dataTree, 
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        $idField, 
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        $parentidField, 
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        $nameField, 
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        $detailDatas, 
        [Parameter(Mandatory=$true, ParameterSetName='simple')]
        $detailsId,
        [Parameter(Mandatory=$false, ParameterSetName='simple')]
        [Parameter(Mandatory=$false, ParameterSetName='customId')]
        [String[]]$hiddenFields,
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        [string]$idMappingQuery,
        [Parameter(Mandatory=$true, ParameterSetName='customId')]
        [string]$parentMappingQuery,
        [Parameter(Mandatory=$false, ParameterSetName='simple')]
        [Parameter(Mandatory=$false, ParameterSetName='customId')]
        [String]$itemAlertQuery, 
        [Parameter(Mandatory=$false, ParameterSetName='simple')]
        [Parameter(Mandatory=$false, ParameterSetName='customId')]
        [switch]$showAlertInHierarchy, 
        [Parameter(Mandatory=$false, ParameterSetName='simple')]
        [Parameter(Mandatory=$false, ParameterSetName='customId')]
        [switch]$showOnlyAlertItems
    )

    $originalIdName = $idField

    # Pre-Processing :
    # - transform id & parentid fields
    # - use custom query to match item and parent
    # - generate unique id to avoid rendering issues (ie. when the id is an URL)
    
    # Create HashTable of computed id query -> ID (to compute hierarchy = detect parent)
    $htComputedId = @{}
    # Create HashTable of nativeId -> ID (to assign ID for mapping between tables)
    $htOriginalId = @{}
    if ($PSCmdlet.ParameterSetName -eq 'customId') {
        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Create ID mapping HashTable"
        
        foreach ($nativeId in $dataTree |select -ExpandProperty $idField) {
            $id = "url$(get-randomId)"
            $htOriginalId[$nativeId] = $id
            # "'{0}' -replace '/sites/', '/' -replace '/lists/', '/'"
            $computedId = Invoke-Expression ($idMappingQuery.toString() -f ($nativeId -replace "'", "''"))
            $htComputedId[$computedId] = $id
        }

        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Compute parent hierarchy"
        $index = 0
        foreach ($s in $dataTree) { 
            write-progress -activity "Processing Hierarchy" -status "$($s.$idField)" -PercentComplete ($index*100/($dataTree.count)) 
            # "(({0}.url -split '/') |select -SkipLast 1|? {@('sites','lists') -notcontains $_}) -join '/'"
            $parentId = $null
            if ($parentMappingQuery) {
                #Write-Verbose "Parent Mapping: $($parentMappingQuery.toString() -f $s.$idField)"
                $parentId = Invoke-Expression ($parentMappingQuery.toString() -f ($s.$idField -replace "'", "''")) 
            } else {
                $parentId = $s.$idField
            }
            #$parentId = $synthetic |? {$_.url -like $parentUri}
            #$s |Add-Member -MemberType NoteProperty -Name 'parent' -Value "$($parentId.id)"
            $s |Add-Member -MemberType NoteProperty -Name 'parent' -Value $htComputedId[$parentId]
            $s |Add-Member -MemberType NoteProperty -Name 'id' -Value $htOriginalId[$s.$idField]
            $index++
            
        }
        $idField = 'id'
        $parentIdField = 'parent'
        write-progress -activity "Processing Hierarchy" -completed
    }

    # Create HashTable of ID -> Object (to boost icon assignment on parent item)
    $htId = @{}
    if ($PSCmdlet.ParameterSetName -eq 'customId') {
        foreach ($s in $dataTree) { $htId[$htOriginalId[$s.$originalIdName]] = $s }
    } else {
        foreach ($s in $dataTree) { $htId[$s.$originalIdName] = $s }
    }

    # Process Alerting on treeview
    if ($itemAlertQuery) {
        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Compute tree icons"
        $index = 0
        foreach ($item in $dataTree) {
            write-progress -activity "Processing icons" -status "$($item.$nameField)" -PercentComplete ($index*100/($dataTree.count)) 
            # "'({0}.PermissionsUniques' -eq 'Oui'"
            if (Invoke-Expression ($itemAlertQuery.toString() -f (($item|convertto-json) -replace "'", "''"))) {
                $item |Add-Member -MemberType NoteProperty -Name 'icon' -Value 'fas fa-lock-open' -force
                $item |Add-Member -MemberType NoteProperty -Name 'icon_color' -Value 'red' -force
                if ($showAlertInHierarchy) {
                    while ($null -ne $item) {
                        try { 
                            $item = $htId[$item.$parentIdField]
                        } catch { $item = $null }
                        $item |% { 
                            $_ |Add-Member -MemberType NoteProperty -Name 'icon' -Value 'fas fa-exclamation' -ea Silentlycontinue
                        }
                    }
                }
            }
            $index++
        }
        write-progress -activity "Processing icon" -completed
    }

    # Rendering filtering
    if ($showOnlyAlertItems) {
        $dataTree = $dataTree |? {$null -ne $_.icon}
        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Filtered tree data to impacted elements and hierarchy : $($dataTree.count)"
        $dataFilteredList = $dataTree |select -ExpandProperty $originalIdName
        $detailDatas = $detailDatas |? {$dataFilteredList -contains $_.$originalIdName}
        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Filtered details data to impacted elements : $($detailDatas.count)"
    }

    # Associate New ID to detailed data
    if ($PSCmdlet.ParameterSetName -eq 'customId') {
        $total = @($detailDatas).count
        Write-Verbose "new-htmlItemTreeWithDetailedTable -> Associate IDs"
        $index = 0
        # Add correponding IDs
        foreach ($d in $detailDatas) { 
            write-progress -activity "Associating ID on Detailed data" -status "$($d.$originalIdName)" -PercentComplete ($index*100/$total) 
            #$s = $synthetic |? {$_.url -like $d.url}
            $d |Add-Member -MemberType NoteProperty -Name 'id' -Value $htOriginalId[$d.$originalIdName]
            $index++
        }
        $detailsId = 'id'
        write-progress -activity "Processing ID" -completed
    }

    # Update hidden fields on detail table to hide id and originalId
    @($idField, $originalIdName) |% {
        if ($hiddenFields -notcontains $_) {
            $hiddenFields += $_
        }
    }

    # Process Rendering
    Write-Verbose "new-htmlItemTreeWithDetailedTable -> Rendering HTML TreeView"
    $htmlItem = new-object -typename htmlItem

    $htmlItem.htmlContent += "<div class='container card-body row align-items-start'>
        <div class='col-1 treeview-animated w-20 border mx-4 my-4'>
        <ul class='treeview-animated-list mb-3'>"

    $itemsToProcess = @()
    # Start with root items
    $itemsToProcess += $dataTree |? { ($null -eq $_.$parentidField) -or ('' -eq $_.$parentidField.trim()) `
        -or ($dataTree.$idField -notcontains $_.$parentidField) }
    $progressId = Get-Random
    $progresscount = new-variable -name "progress_treeview_$progressId" -value 0 -scope global
    $totalItems = @($dataTree).count
    foreach ($item in $itemsToProcess) {
        $progress = get-variable -name "progress_treeview_$progressId" -valueonly
        write-progress -id $progressId -activity "Building TreeView" -PercentComplete ($progresst*100/$totalItems)
        $progress++
        set-variable -name "progress_treeview_$progressId" -value $progress -force -scope global

        $htmlItem.htmlContent += format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField `
            -nameField $nameField -parentidValue $item.$idField -method 'showTreeDetailed' `
            -progressId $progressId -progressCountVariable "progress_treeview_$progressId" -progressTotal $totalItems
        
    }
    write-progress -id $progressId -activity "Building TreeView" -completed
    $htmlItem.javascriptOnLoad += "`$('.treeview-animated').mdbTreeview();"
    

    $keys = $detailDatas |select-object -excludeproperty icon, icon_color |Get-Member -MemberType noteproperty |select -ExpandProperty name -unique
    if ($null -eq $keys) { $keys = $detailDatas.keys |select -unique }
    $htmlItem.htmlContent += "
        </ul>
        </div>
        <div class='col w-20 mx-4 my-4 w-auto'>
            <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'>
            <thead class='table-primary'>
                <tr>
                    $($keys |foreach-object { "<th>$_</th>" })
                </tr>
            </thead>
            <tbody></tbody>
            </table></div>
            <script>
             
            function showTreeDetailed(elementId)
            {
                var dataTables = `$('#$($htmlItem.id)').DataTable()
                dataTables.columns($(@($keys|%{$_.tolower()}).indexof($detailsId.tolower()) )).search(elementId).draw();
            }
             
            </script>
        </div>"

    $htmlItem.javascriptOnLoad += "var jsonData = $($detailDatas |ConvertTo-Json);
        `$('#$($htmlItem.id)').DataTable({
        data: jsonData,
        columns: [
            $(($keys |% { "{ data: '$_' }"}) -join ',')
        ],
        columnDefs: [
            {
                targets: '_all',
                'render': function (data, type, row, meta ) {
                    if(type === 'display'){
                        let aStr = ('' + data).split(',')
                        if (aStr.length > 1) {
                            var str = '';
                            aStr.forEach((item)=>{
                                str += item + '<br/>';
                            });
                            return str;
                        } else {
                            return data
                        }
                    }else{
                        return data;
                    }
                }
            },
            $(if ($hiddenFields) {
                $keys = $keys |% {$_.toLower()}
                ($hiddenFields |% { if ($(@($keys).indexof($_.toLower()) -gt -1)) { "{
                    targets: [ $(@($keys).indexof($_.toLower()) ) ],
                    visible: false
                } "}}) -join ','
            })
        ],
        searchHighlight: true,
    });"

    #$htmlItem.javascriptOnLoad += "`$('#$($htmlItem.id) tr').toggle();"
    return $htmlItem
}

function add-htmlItemNewRowAfter {
    param(
        [htmlItem]$item
    )
    $item.newlineAfter = $true
}

function new-htmlItemChart {
    param(
        [string]$title, 
        [Parameter(Mandatory)]
        $data,
        [String]$linkedTableId,
        [String]$linkedFieldName,
        [Parameter(Mandatory)]
        [ValidateSet("pie","bar","doughnut")]
        [String]$chartType,
        [ValidateSet("left","right","top","bottom")]
        [String]$legendPosition = 'right',
        [switch]$linkedTableExactMatch
    )
    $htmlItem = new-object -typename htmlItem       
    $config = @{
        type= $chartType
        data= @{
            labels= $data.keys #@('January', 'February', 'March', 'April', 'May', 'June', 'July')
            datasets= @(
                @{
                    data= $data.values #@(1, 65, 45, 65, 35, 65, 30)
                    backgroundColor= @("#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#4D5360", "#E57373", "#F06292", "#BA68C8", "#9575CD", "#7986CB", "#64B5F6", "#4DD0E1", "#4DB6AC", "#81C784", "#AED581", "#DCE775", "#FFD54F", "#FFB74D", "#A1887F", "#90A4AE") |select -first ($data.count)
                    hoverBackgroundColor= @("#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#616774", "#EF9A9A", "#F48FB1", "#CE93D8", "#B39DDB", "#9FA8DA", "#90CAF9", "#80DEEA", "#80CBC4", "#A5D6A7", "#C5E1A5", "#E6EE9C", "#FFE082", "#FFCC80", "#BCAAA4", "#B0BEC5") |select -first ($data.count)
                    borderWidth= @(1, 1, 1, 1, 1)
                }
            )
        }
        options= @{
            responsive= $true
            title= @{
                display= $true
                text= $title
            }
            tooltips= @{
                mode= 'index'
                intersect= $false
            }
            hover= @{
                mode= 'nearest'
                intersect= $true
            }
            legend= 
                if ($chartType -eq 'bar') { 
                    @{display = $false}
                } else {
                    @{position= $legendPosition}
                }
            
            interaction= @{
                mode= 'dataset'
            }

        }
    }

    # HTML Content to return
    $htmlItem.htmlContent += "<div class='col-md-4 py-1'>
    <div class='card'>
        <div class='card-body'>
            <canvas id='canvas_$($htmlItem.id)'></canvas>
        </div>
    </div>
    </div>
    <script>
    var config_$($htmlItem.id) = $($config |ConvertTo-Json -Depth 10)
    var ctx_$($htmlItem.id) = document.getElementById('canvas_$($htmlItem.id)').getContext('2d');
    window.myLine_$($htmlItem.id) = new Chart(ctx_$($htmlItem.id), config_$($htmlItem.id));
    $(
        if ($linkedTableId) {
            "document.getElementById('canvas_$($htmlItem.id)').onclick =
            function(evt){
                var firstPoint = window.myLine_$($htmlItem.id).getElementAtEvent(evt)[0];
         
                if (firstPoint) {
                    var label = window.myLine_$($htmlItem.id).data.labels[firstPoint._index];
                    var linkedDataTable = `$('#$linkedTableId').DataTable()
                    linkedDataTable.columns().eq(0).each( function (colIdx) {
                        var title = linkedDataTable.columns(colIdx).header();
                        linkedDataTable.columns(colIdx).search('').draw();
                        if (`$(title).html() == '$linkedFieldName') {
                            if('$linkedTableExactMatch' == 'True') {
                                linkedDataTable.columns(colIdx).search('^' + label + '$', true, false, true).draw();
                            } else {
                                linkedDataTable.columns(colIdx).search(label).draw();
                            }
                        }
                    });
                } else {
                    var linkedDataTable = `$('#$linkedTableId').DataTable()
                    linkedDataTable.columns().eq(0).each( function (colIdx) {
                        var title = linkedDataTable.columns(colIdx).header();
                        if (`$(title).html() == 'Name') {
                            linkedDataTable.columns(colIdx).search('').draw();
                        }
                    });
                }
            };"
        }
    )
    </script>"

    return $htmlItem
}

class htmlItem {
    [string]$id = 'item' + (get-randomId -length 12)
    [String]$htmlContent
    [string]$javascriptToLoad
    [string]$javascriptOnLoad
    [bool]$newlineAfter = $false
}

<#
#
# HTML Section
#
#>


function new-htmlSection {
    param(
        [String]$title,
        [switch]$foldable,
        [switch]$folded,
        [int]$notifInfoCount = 0,
        [int]$notifSuccessCount = 0,
        [int]$notifWarningCount = 0,
        [int]$notifAlertCount = 0
    )
    $htmlSection = new-object -typename htmlSection
    if ($title) {$htmlSection.title = $title}
    if ($foldable) {$htmlSection.foldable = $true}
    if ($folded) {$htmlSection.folded = $true}
    $htmlSection.notif_info = $notifInfoCount
    $htmlSection.notif_success = $notifSuccessCount
    $htmlSection.notif_warning = $notifWarningCount
    $htmlSection.notif_alert = $notifAlertCount
    return $htmlSection
}

function add-htmlSectionItem {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [htmlSection]$section,
        [htmlItem[]]$itemList
    )
    foreach ($item in $itemList) {
        $section.addHtmlItem($item)
    }
    
}

class htmlSection {
    [string]$id = 'section' + (get-randomId -length 12)
    [string]$title
    [string]$data
    [string]$htmlCharts
    [bool]$hasTree=$false
    [string[]]$javascriptToLoad
    [string[]]$javascriptOnLoad
    [htmlItem[]]$itemList
    [bool]$foldable = $false
    [bool]$folded = $false
    [int]$notif_info = 0
    [int]$notif_success = 0
    [int]$notif_warning = 0
    [int]$notif_alert = 0
    
    [void]addHtmlContent($htmlContent) {
        $this.data += $htmlContent
    }

    [void]addHtmlItem([htmlItem]$item) {
        $this.itemList += $item
    }

    [string]toHtml() {
        $collapsing = ""
        $defaultShow = ""
        $sectionId = "section$(get-randomId)"
        if ($this.foldable) { 
            $collapsing = "collapse"
            if ($this.folded -eq $false) { 
                $defaultShow = "show"
            }
        }
        
        return "
        <div class='card'>
            <h2 class='card-header'><a href='#$sectionId' data-bs-toggle='$collapsing'>$($this.title)</a>
            $(if ($this.notif_info -gt 0) { "<span class='badge-pill badge-info'>$($this.notif_info)</span>"})
            $(if ($this.notif_success -gt 0) { "<span class='badge-pill badge-success'>$($this.notif_success)</span>"})
            $(if ($this.notif_warning -gt 0) { "<span class='badge-pill badge-warning'>$($this.notif_warning)</span>"})
            $(if ($this.notif_alert -gt 0) { "<span class='badge-pill badge-danger'>$($this.notif_alert)</span>"})
            </h2>
            <div id='$sectionId' class='card-body row align-items-start $collapsing $defaultShow'>
            $(foreach ($item in $this.itemList) {
" $($item.htmlContent)"
                if ($item.newlineAfter) { "
                </div>
                <div id='$sectionId' class='card-body row align-items-start $collapsing $defaultShow'>
                "}
                $this.javascriptOnLoad += $item.javascriptOnLoad
                $this.javascriptToLoad += $item.javascriptToLoad
            })
            </div>
        </div>"

    }
}

<#
#
# HTML Report
#
#>


function new-htmlReport {
    param(
        [string]$title,
        [string]$subtitle,
        [string]$logoFile
    )
    $htmlreport = new-object -typename htmlReport
    if ($title) {$htmlreport.title = $title}
    if ($subtitle) {$htmlreport.subtitle = $subtitle}
    if ($logoFile) {$htmlReport.addLogo($logoFile)}
    return $htmlreport
}

function add-htmlReportSection {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [htmlReport]$report,
        [htmlSection[]]$sectionList
    )

    foreach ($section in $sectionList) {
        $report.sections += $section
    }
}

class htmlReport {
    [string]$title
    [string]$subtitle
    [htmlSection[]]$sections
    [string]$logoBase64

    [string]toHtml() {
        $result = "<!DOCTYPE html>
        <html lang='en'>
            <head>
            <title>$($this.title)</title>
            <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css' integrity='sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC' crossorigin='anonymous'>
            <link rel='stylesheet' href='https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap5.min.css'>
            <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.9.0/css/mdb.css'>
            <link rel='stylesheet' href='https://cdn.datatables.net/plug-ins/1.10.25/features/searchHighlight/dataTables.searchHighlight.css'>
            <script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'></script>
            <script src='https://kit.fontawesome.com/3bf1dda615.js' crossorigin='anonymous'></script>
            <style type='text/css'>
                .card-body,.card-header {
                    padding: 0.5em ;
                    word-break: keep-all;
                }
                .jumbotron {
                    padding-top: 1em;
                    padding-bottom: 1em;
                }
                .card {
                    margin-bottom: 0.5em;
                }
                .visually-hidden {
                    display: none;
                }
                canvas{
                    -moz-user-select: none;
                    -webkit-user-select: none;
                    -ms-user-select: none;
         
                }
                .treeview-animated {
                    max-height: 500px;
                    overflow-y: scroll;
                }
                table {
                    width: 100%;
                }
            </style>
            </head>
            <body>
            <div class='navbar navbar-light'>
                <img class='navbar-brand' src='$($this.logoBase64)' style='max-width:100%;'/>
                <div class='col'>
                            <h1 class='display-4'>$($this.title)</h1>"

            if ($this.subtitle) {
                $result += "<p class='lead'>$($this.subtitle)</p>" }
            $result += "<p class='display-8'>Generated on $(Get-Date)</p>
                </div>
            </div>
        "

        $result += $this.sections | ForEach-Object { $_.toHtml() }
                
        $result += '
        <script src=''https://code.jquery.com/jquery-3.6.0.min.js'' integrity=''sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4='' crossorigin=''anonymous''></script>
        <script src=''https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js'' integrity=''sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF'' crossorigin=''anonymous''></script>
        <script src=''https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.9.0/js/mdb.min.js''></script>
        <script src=''https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js''></script>
        <script src=''https://cdn.datatables.net/1.10.25/js/dataTables.bootstrap4.min.js''></script>
        <script src=''https://bartaz.github.io/sandbox.js/jquery.highlight.js''></script>
        <script src=''https://cdn.datatables.net/plug-ins/1.10.25/features/searchHighlight/dataTables.searchHighlight.min.js''></script>
        '

        # Add Custom script resources
        $_.javascriptToLoad |% { $result += $_ }
        $result += '<script>$(document).ready(function() {'
        if ($this.sections.hasTree -contains $true) {
            $result +=  "`$('.treeview-animated').mdbTreeview();"
        }
        # Add onLoad calls
        $result += $this.sections | ForEach-Object { $_.javascriptOnLoad }
        $result += ' });</script>'
       
        $result += '</body>
        </html>'


        return $result
    }

    [void]addLogo([string]$filename) {
        if ((test-path $filename) -eq $false) { write-warning "Failed opening logo image: file not found." }
        else {
            $file = get-item $filename
            $this.logoBase64 = "data:image/"
            $extension = $file.name -split '\.' |select -last 1
            $this.logoBase64 += $extension
            $this.logoBase64 += ";base64,"
            $this.logoBase64 += [convert]::ToBase64String((get-content $filename -encoding byte))
        }
    }

    [void]toFile([string]$filename) {
        $this.toHtml() |out-file -FilePath $filename -Encoding utf8
    }
}