Views/Reports.ps1
|
function Show-InTUIReportsView { <# .SYNOPSIS Displays the Reports view with stale devices, app install failures, and license utilization. #> [CmdletBinding()] param() $exitView = $false while (-not $exitView) { Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports') $reportChoices = @( 'Stale Devices Report', 'Stale Users Report', 'App Install Failures', 'License Utilization', 'Compliance Trend Chart', 'Enrollment Trend Chart', '-------------', 'Back to Home' ) $selection = Show-InTUIMenu -Title "[DarkOrange]Reports[/]" -Choices $reportChoices Write-InTUILog -Message "Reports view selection" -Context @{ Selection = $selection } switch ($selection) { 'Stale Devices Report' { Show-InTUIStaleDevicesReport } 'Stale Users Report' { Show-InTUIStaleUsersReport } 'App Install Failures' { Show-InTUIAppInstallFailures } 'License Utilization' { Show-InTUILicenseUtilization } 'Compliance Trend Chart' { Show-InTUIComplianceTrendChart } 'Enrollment Trend Chart' { Show-InTUIEnrollmentTrendChart } 'Back to Home' { $exitView = $true } default { continue } } } } function Show-InTUIStaleDevicesReport { <# .SYNOPSIS Displays a report of devices that have not synced within a specified number of days. #> [CmdletBinding()] param() Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'Stale Devices') $daysInput = Read-InTUITextInput -Message "[DarkOrange]Enter days threshold for stale devices[/]" -DefaultAnswer "30" $days = 30 if ($daysInput -match '^\d+$') { $days = [int]$daysInput } Write-InTUILog -Message "Running stale devices report" -Context @{ DaysThreshold = $days } $cutoff = [DateTime]::UtcNow.AddDays(-$days).ToString('yyyy-MM-ddTHH:mm:ssZ') $devices = Show-InTUILoading -Title "[DarkOrange]Loading stale devices...[/]" -ScriptBlock { Get-InTUIPagedResults -Uri '/deviceManagement/managedDevices' -Beta -PageSize 50 ` -Filter "lastSyncDateTime le $cutoff" ` -Select 'id,deviceName,operatingSystem,lastSyncDateTime,userPrincipalName,complianceState,managedDeviceOwnerType' } if ($null -eq $devices -or $devices.Results.Count -eq 0) { Show-InTUIWarning "No stale devices found (threshold: $days days)." Read-InTUIKey return } Write-InTUILog -Message "Stale devices found" -Context @{ Count = $devices.Results.Count; DaysThreshold = $days } $rows = @() foreach ($device in $devices.Results) { $daysSinceSync = ([DateTime]::UtcNow - [DateTime]::Parse($device.lastSyncDateTime)).Days $lastSync = Format-InTUIDate -DateString $device.lastSyncDateTime $compColor = switch ($device.complianceState) { 'compliant' { 'green' } 'noncompliant' { 'red' } default { 'yellow' } } $user = if ($device.userPrincipalName) { $device.userPrincipalName } else { 'Unassigned' } $rows += , @( $device.deviceName, ($device.operatingSystem ?? 'N/A'), $lastSync, "$daysSinceSync", $user, "[$compColor]$($device.complianceState)[/]" ) } Show-InTUIStatusBar -Total $devices.TotalCount -Showing $devices.Results.Count -FilterText "Stale > $days days" Show-InTUISortableTable -Title "Stale Devices Report" -Columns @('Device Name', 'OS', 'Last Sync', 'Days Since Sync', 'User', 'Compliance') -Rows $rows -BorderColor DarkOrange } function Show-InTUIAppInstallFailures { <# .SYNOPSIS Displays app install failure report by letting the user select an app and viewing failed device statuses. #> [CmdletBinding()] param() $exitReport = $false while (-not $exitReport) { Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'App Install Failures') Write-InTUILog -Message "Loading apps for install failure report" $apps = Show-InTUILoading -Title "[DarkOrange]Loading apps...[/]" -ScriptBlock { Get-InTUIPagedResults -Uri '/deviceAppManagement/mobileApps' -Beta -PageSize 50 ` -Select 'id,displayName' } if ($null -eq $apps -or $apps.Results.Count -eq 0) { Show-InTUIWarning "No apps found." Read-InTUIKey $exitReport = $true continue } $appChoices = @() foreach ($app in $apps.Results) { $appType = Get-InTUIAppTypeFriendlyName -ODataType $app.'@odata.type' $appChoices += "[white]$(ConvertTo-InTUISafeMarkup -Text $app.displayName)[/] [grey]| $appType[/]" } $choiceMap = Get-InTUIChoiceMap -Choices $appChoices $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back') Show-InTUIStatusBar -Total $apps.TotalCount -Showing $apps.Results.Count $selection = Show-InTUIMenu -Title "[DarkOrange]Select an app to check failures[/]" -Choices $menuChoices if ($selection -eq 'Back') { $exitReport = $true } elseif ($selection -ne '─────────────') { $idx = $choiceMap.IndexMap[$selection] if ($null -ne $idx -and $idx -lt $apps.Results.Count) { $selectedApp = $apps.Results[$idx] Show-InTUIAppFailureDetail -AppId $selectedApp.id -AppName $selectedApp.displayName } } } } function Show-InTUIAppFailureDetail { <# .SYNOPSIS Displays failed device install statuses for a specific app. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AppId, [Parameter(Mandatory)] [string]$AppName ) Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'App Install Failures', $AppName) Write-InTUILog -Message "Loading install failures for app" -Context @{ AppId = $AppId; AppName = $AppName } $statuses = Show-InTUILoading -Title "[DarkOrange]Loading device statuses for $AppName...[/]" -ScriptBlock { Invoke-InTUIGraphRequest -Uri "/deviceAppManagement/mobileApps/$AppId/deviceStatuses?`$top=50" -Beta } if (-not $statuses.value) { Show-InTUISuccess "No install status data available for this app." Read-InTUIKey return } $failures = @($statuses.value | Where-Object { $_.installState -eq 'failed' }) if ($failures.Count -eq 0) { Show-InTUISuccess "No install failures for this app." Read-InTUIKey return } Write-InTUILog -Message "App install failures found" -Context @{ AppName = $AppName; FailureCount = $failures.Count } $exitDrillDown = $false while (-not $exitDrillDown) { Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'App Install Failures', $AppName) $rows = @() $failureChoices = @() foreach ($status in $failures) { $rows += , @( ($status.deviceName ?? 'N/A'), "[red]$($status.installState)[/]", ($status.errorCode ?? 'N/A'), ($status.userPrincipalName ?? 'N/A'), (Format-InTUIDate -DateString $status.lastSyncDateTime) ) $errorHex = if ($status.errorCode) { '0x{0:X8}' -f [int64]$status.errorCode } else { 'N/A' } $failureChoices += "[white]$(ConvertTo-InTUISafeMarkup -Text ($status.deviceName ?? 'N/A'))[/] [grey]| $errorHex[/]" } Show-InTUIStatusBar -Total $failures.Count -Showing $failures.Count -FilterText "Failed installs" Render-InTUITable -Title "Install Failures - $AppName" -Columns @('Device', 'Status', 'Error Code', 'User', 'Last Sync') -Rows $rows -BorderColor DarkOrange $choiceMap = Get-InTUIChoiceMap -Choices $failureChoices $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back') $selection = Show-InTUIMenu -Title "[DarkOrange]Select a failure for details[/]" -Choices $menuChoices if ($selection -eq 'Back') { $exitDrillDown = $true } elseif ($selection -ne '─────────────') { $idx = $choiceMap.IndexMap[$selection] if ($null -ne $idx -and $idx -lt $failures.Count) { $failureStatus = $failures[$idx] $errorHex = if ($failureStatus.errorCode) { '0x{0:X8}' -f [int64]$failureStatus.errorCode } else { 'N/A' } $errorInfo = Get-InTUIErrorCodeInfo -ErrorCode $errorHex Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'App Install Failures', $AppName, ($failureStatus.deviceName ?? 'N/A')) if ($errorInfo) { $drillContent = @" [bold white]Error Code:[/] $errorHex [grey]Description:[/] [white]$($errorInfo.Description)[/] [grey]Category:[/] [yellow]$($errorInfo.Category)[/] [bold]Remediation:[/] [green]$($errorInfo.Remediation)[/] [grey]Device:[/] $($failureStatus.deviceName ?? 'N/A') [grey]User:[/] $($failureStatus.userPrincipalName ?? 'N/A') [grey]Last Sync:[/] $(Format-InTUIDate -DateString $failureStatus.lastSyncDateTime) "@ } else { $drillContent = @" [bold white]Error Code:[/] $errorHex [yellow]Unknown error code.[/] Check Microsoft Intune troubleshooting docs for this error code. Review IME logs on the device: C:\ProgramData\Microsoft\IntuneManagementExtension\Logs [grey]Device:[/] $($failureStatus.deviceName ?? 'N/A') [grey]User:[/] $($failureStatus.userPrincipalName ?? 'N/A') [grey]Last Sync:[/] $(Format-InTUIDate -DateString $failureStatus.lastSyncDateTime) "@ } Show-InTUIPanel -Title "[DarkOrange]Error Details[/]" -Content $drillContent -BorderColor DarkOrange Read-InTUIKey } } } } function Show-InTUILicenseUtilization { <# .SYNOPSIS Displays license utilization report showing assigned vs available licenses per SKU. #> [CmdletBinding()] param() Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'License Utilization') Write-InTUILog -Message "Loading license utilization data" $response = Show-InTUILoading -Title "[DarkOrange]Loading license data...[/]" -ScriptBlock { Invoke-InTUIGraphRequest -Uri '/subscribedSkus' } if ($null -eq $response -or -not $response.value) { Show-InTUIWarning "No license data available." Read-InTUIKey return } Write-InTUILog -Message "License data loaded" -Context @{ SKUCount = @($response.value).Count } $rows = @() foreach ($sku in $response.value) { $skuName = $sku.skuPartNumber $total = $sku.prepaidUnits.enabled $consumed = $sku.consumedUnits $available = $total - $consumed if ($total -gt 0) { $utilization = [math]::Round(($consumed / $total) * 100, 1) } else { $utilization = 0 } $utilColor = if ($utilization -gt 90) { 'red' } elseif ($utilization -gt 70) { 'yellow' } else { 'green' } $rows += , @( $skuName, "$total", "$consumed", "$available", "[$utilColor]$utilization%[/]" ) } Show-InTUISortableTable -Title "License Utilization" -Columns @('License', 'Total', 'Assigned', 'Available', 'Utilization %') -Rows $rows -BorderColor DarkOrange } function Show-InTUIComplianceTrendChart { <# .SYNOPSIS Displays a bar chart of device compliance states. #> [CmdletBinding()] param() Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'Compliance Trend Chart') Write-InTUILog -Message "Loading compliance chart data" $data = Show-InTUILoading -Title "[DarkOrange]Loading compliance data...[/]" -ScriptBlock { Invoke-InTUIGraphRequest -Uri '/deviceManagement/deviceCompliancePolicyDeviceStateSummary' -Beta } if ($null -eq $data) { Show-InTUIWarning "Could not load compliance data." Read-InTUIKey return } Write-InTUILog -Message "Compliance chart data loaded" -Context @{ Compliant = $data.compliantDeviceCount NonCompliant = $data.nonCompliantDeviceCount } # Build chart data $chartData = @( @{ Label = "Compliant"; Value = ($data.compliantDeviceCount ?? 0); Color = "green" } @{ Label = "Non-Compliant"; Value = ($data.nonCompliantDeviceCount ?? 0); Color = "red" } @{ Label = "In Grace Period"; Value = ($data.inGracePeriodCount ?? 0); Color = "yellow" } @{ Label = "Conflict"; Value = ($data.conflictDeviceCount ?? 0); Color = "orange" } @{ Label = "Error"; Value = ($data.errorCount ?? 0); Color = "red" } @{ Label = "Not Evaluated"; Value = ($data.notEvaluatedDeviceCount ?? 0); Color = "grey" } ) # Filter out zero values for cleaner display $chartData = @($chartData | Where-Object { $_.Value -gt 0 }) if ($chartData.Count -eq 0) { Show-InTUIWarning "No compliance data to display." Read-InTUIKey return } Render-InTUIBarChart -Title "Compliance Distribution" -Items $chartData # Summary table $total = ($data.compliantDeviceCount ?? 0) + ($data.nonCompliantDeviceCount ?? 0) + ($data.inGracePeriodCount ?? 0) + ($data.conflictDeviceCount ?? 0) + ($data.errorCount ?? 0) + ($data.notEvaluatedDeviceCount ?? 0) $complianceRate = if ($total -gt 0) { [math]::Round((($data.compliantDeviceCount ?? 0) / $total) * 100, 1) } else { 0 } $summaryContent = @" [grey]Total Devices:[/] [white]$total[/] [grey]Compliance Rate:[/] [white]$complianceRate%[/] [grey]Compliant:[/] [green]$($data.compliantDeviceCount ?? 0)[/] [grey]Non-Compliant:[/] [red]$($data.nonCompliantDeviceCount ?? 0)[/] "@ Show-InTUIPanel -Title "[DarkOrange]Summary[/]" -Content $summaryContent -BorderColor DarkOrange Read-InTUIKey } function Show-InTUIEnrollmentTrendChart { <# .SYNOPSIS Displays a bar chart of device enrollment by OS platform. #> [CmdletBinding()] param() Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'Enrollment Trend Chart') Write-InTUILog -Message "Loading enrollment chart data" $data = Show-InTUILoading -Title "[DarkOrange]Loading enrollment data...[/]" -ScriptBlock { Invoke-InTUIGraphRequest -Uri '/deviceManagement/managedDeviceOverview' -Beta } if ($null -eq $data -or $null -eq $data.deviceOperatingSystemSummary) { Show-InTUIWarning "Could not load enrollment data." Read-InTUIKey return } $osSummary = $data.deviceOperatingSystemSummary Write-InTUILog -Message "Enrollment chart data loaded" -Context @{ Windows = $osSummary.windowsCount iOS = $osSummary.iosCount macOS = $osSummary.macOSCount Android = $osSummary.androidCount } # Build chart data $chartData = @( @{ Label = "Windows"; Value = ($osSummary.windowsCount ?? 0); Color = "blue" } @{ Label = "iOS/iPadOS"; Value = ($osSummary.iosCount ?? 0); Color = "grey" } @{ Label = "macOS"; Value = ($osSummary.macOSCount ?? 0); Color = "grey" } @{ Label = "Android"; Value = ($osSummary.androidCount ?? 0); Color = "green" } @{ Label = "Linux"; Value = ($osSummary.linuxCount ?? 0); Color = "yellow" } ) # Filter out zero values $chartData = @($chartData | Where-Object { $_.Value -gt 0 }) if ($chartData.Count -eq 0) { Show-InTUIWarning "No enrollment data to display." Read-InTUIKey return } Render-InTUIBarChart -Title "Enrollment by Platform" -Items $chartData # Summary table $total = $data.enrolledDeviceCount ?? 0 $summaryContent = @" [grey]Total Enrolled:[/] [white]$total[/] [grey]MDM Enrolled:[/] [white]$($data.mdmEnrolledCount ?? 0)[/] [grey]Dual Enrolled:[/] [white]$($data.dualEnrolledDeviceCount ?? 0)[/] [bold]By Platform:[/] [blue]Windows:[/] $($osSummary.windowsCount ?? 0) [grey]iOS/iPadOS:[/] $($osSummary.iosCount ?? 0) [grey]macOS:[/] $($osSummary.macOSCount ?? 0) [green]Android:[/] $($osSummary.androidCount ?? 0) [yellow]Linux:[/] $($osSummary.linuxCount ?? 0) "@ Show-InTUIPanel -Title "[DarkOrange]Enrollment Summary[/]" -Content $summaryContent -BorderColor DarkOrange Read-InTUIKey } function Show-InTUIStaleUsersReport { <# .SYNOPSIS Displays a report of users who have not signed in within a specified number of days. #> [CmdletBinding()] param() Clear-Host Show-InTUIHeader Show-InTUIBreadcrumb -Path @('Home', 'Reports', 'Stale Users') $daysInput = Read-InTUITextInput -Message "[DarkOrange]Enter days threshold for stale users[/]" -DefaultAnswer "90" $days = 90 if ($daysInput -match '^\d+$') { $days = [int]$daysInput } Write-InTUILog -Message "Running stale users report" -Context @{ DaysThreshold = $days } $cutoff = [DateTime]::UtcNow.AddDays(-$days).ToString('yyyy-MM-ddTHH:mm:ssZ') $users = Show-InTUILoading -Title "[DarkOrange]Loading stale users...[/]" -ScriptBlock { Get-InTUIPagedResults -Uri '/users' -PageSize 50 ` -Filter "signInActivity/lastSignInDateTime le $cutoff" ` -Select 'id,displayName,userPrincipalName,signInActivity,assignedLicenses' ` -Headers @{ ConsistencyLevel = 'eventual' } ` -IncludeCount } if ($null -eq $users -or $users.Results.Count -eq 0) { Show-InTUIWarning "No stale users found (threshold: $days days)." Read-InTUIKey return } Write-InTUILog -Message "Stale users found" -Context @{ Count = $users.Results.Count; DaysThreshold = $days } $rows = @() foreach ($user in $users.Results) { $lastSignIn = $user.signInActivity.lastSignInDateTime $daysSince = if ($lastSignIn) { ([DateTime]::UtcNow - [DateTime]::Parse($lastSignIn)).Days } else { 'Never' } $formattedDate = Format-InTUIDate -DateString $lastSignIn $licenseCount = if ($user.assignedLicenses) { @($user.assignedLicenses).Count } else { 0 } $rows += , @( ($user.displayName ?? 'N/A'), ($user.userPrincipalName ?? 'N/A'), $formattedDate, "$daysSince", "$licenseCount" ) } Show-InTUIStatusBar -Total $users.TotalCount -Showing $users.Results.Count -FilterText "Stale > $days days" Show-InTUISortableTable -Title "Stale Users Report" -Columns @('Display Name', 'UPN', 'Last Sign-In', 'Days Since', 'Licenses') -Rows $rows -BorderColor DarkOrange } |