Views/Groups.ps1

function Get-InTUIGroupType {
    <#
    .SYNOPSIS
        Determines the friendly group type name with icon.
    #>

    param($Group)

    if ($Group.groupTypes -contains 'DynamicMembership') {
        if ($Group.securityEnabled) { return "[cyan]Dynamic Security[/]" }
        else { return "[cyan]Dynamic M365[/]" }
    }
    elseif ($Group.securityEnabled -and -not $Group.mailEnabled) {
        return "[blue]Security[/]"
    }
    elseif ($Group.mailEnabled -and $Group.securityEnabled) {
        return "[green]Mail-enabled Security[/]"
    }
    elseif ($Group.groupTypes -contains 'Unified') {
        return "[cyan]Microsoft 365[/]"
    }
    elseif ($Group.mailEnabled) {
        return "[yellow]Distribution[/]"
    }
    else {
        return "[grey]Assigned Security[/]"
    }
}

function Show-InTUIGroupsView {
    <#
    .SYNOPSIS
        Displays the Groups management view mimicking the Intune Groups blade.
    #>

    [CmdletBinding()]
    param()

    $exitView = $false

    while (-not $exitView) {
        Clear-Host
        Show-InTUIHeader
        Show-InTUIBreadcrumb -Path @('Home', 'Groups')

        $groupChoices = @(
            'All Groups',
            'Security Groups',
            'Microsoft 365 Groups',
            'Dynamic Groups',
            'Search Groups',
            '-------------',
            'Back to Home'
        )

        $selection = Show-InTUIMenu -Title "[cyan]Groups[/]" -Choices $groupChoices

        Write-InTUILog -Message "Groups view selection" -Context @{ Selection = $selection }

        switch ($selection) {
            'All Groups' {
                Show-InTUIGroupList
            }
            'Security Groups' {
                Show-InTUIGroupList -TypeFilter 'Security'
            }
            'Microsoft 365 Groups' {
                Show-InTUIGroupList -TypeFilter 'Microsoft365'
            }
            'Dynamic Groups' {
                Show-InTUIGroupList -TypeFilter 'Dynamic'
            }
            'Search Groups' {
                $searchTerm = Read-InTUITextInput -Message "[cyan]Search groups by name[/]"
                if ($searchTerm) {
                    Write-InTUILog -Message "Searching groups" -Context @{ SearchTerm = $searchTerm }
                    Show-InTUIGroupList -SearchTerm $searchTerm
                }
            }
            'Back to Home' {
                $exitView = $true
            }
            default {
                continue
            }
        }
    }
}

function Show-InTUIGroupList {
    <#
    .SYNOPSIS
        Displays a paginated list of groups.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$TypeFilter,

        [Parameter()]
        [string]$SearchTerm
    )

    $exitList = $false

    while (-not $exitList) {
        Clear-Host
        Show-InTUIHeader

        $breadcrumb = @('Home', 'Groups')
        if ($TypeFilter) { $breadcrumb += "$TypeFilter Groups" }
        elseif ($SearchTerm) { $breadcrumb += "Search: $SearchTerm" }
        else { $breadcrumb += 'All Groups' }
        Show-InTUIBreadcrumb -Path $breadcrumb

        $params = @{
            Uri      = '/groups'
            PageSize = 25
            Select   = 'id,displayName,description,groupTypes,mailEnabled,securityEnabled,membershipRule,createdDateTime'
            OrderBy  = 'displayName'
        }

        $filter = @()
        if ($TypeFilter) {
            switch ($TypeFilter) {
                'Security' {
                    $filter += "securityEnabled eq true and mailEnabled eq false"
                }
                'Microsoft365' {
                    $filter += "groupTypes/any(g:g eq 'Unified')"
                }
                'Dynamic' {
                    $filter += "groupTypes/any(g:g eq 'DynamicMembership')"
                }
            }
        }
        if ($SearchTerm) {
            $safe = ConvertTo-InTUISafeFilterValue -Value $SearchTerm
            $filter += "startswith(displayName,'$safe')"
        }

        if ($filter.Count -gt 0) {
            $params['Filter'] = $filter -join ' and '
        }

        $groups = Show-InTUILoading -Title "[cyan]Loading groups...[/]" -ScriptBlock {
            Get-InTUIPagedResults @params
        }

        if ($null -eq $groups -or $groups.Results.Count -eq 0) {
            Show-InTUIWarning "No groups found."
            Read-InTUIKey
            $exitList = $true
            continue
        }

        $groupChoices = @()
        foreach ($group in $groups.Results) {
            $groupType = Get-InTUIGroupType -Group $group
            $desc = if ($group.description) {
                $truncated = $group.description
                if ((Measure-InTUIDisplayWidth -Text $truncated) -gt 40) { $truncated = $truncated.Substring(0, 40) + '...' }
                $truncated
            }
            else { 'No description' }

            $displayName = "$groupType [white]$(ConvertTo-InTUISafeMarkup -Text $group.displayName)[/] [grey]| $desc[/]"
            $groupChoices += $displayName
        }

        $choiceMap = Get-InTUIChoiceMap -Choices $groupChoices
        $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back')

        Show-InTUIStatusBar -Total $groups.TotalCount -Showing $groups.Results.Count -FilterText ($TypeFilter ?? $SearchTerm)

        $selection = Show-InTUIMenu -Title "[cyan]Select a group[/]" -Choices $menuChoices

        if ($selection -eq 'Back') {
            $exitList = $true
        }
        elseif ($selection -ne '─────────────') {
            $idx = $choiceMap.IndexMap[$selection]
            if ($null -ne $idx -and $idx -lt $groups.Results.Count) {
                Show-InTUIGroupDetail -GroupId $groups.Results[$idx].id
            }
        }
    }
}

function Show-InTUIGroupDetail {
    <#
    .SYNOPSIS
        Displays detailed information about a specific group.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$GroupId
    )

    $exitDetail = $false

    while (-not $exitDetail) {
        Clear-Host
        Show-InTUIHeader

        $group = Show-InTUILoading -Title "[cyan]Loading group details...[/]" -ScriptBlock {
            Invoke-InTUIGraphRequest -Uri "/groups/$GroupId`?`$select=id,displayName,description,groupTypes,mailEnabled,securityEnabled,mailNickname,membershipRule,membershipRuleProcessingState,createdDateTime,renewedDateTime,visibility,isAssignableToRole"
        }

        if ($null -eq $group) {
            Show-InTUIError "Failed to load group details."
            Read-InTUIKey
            return
        }

        Show-InTUIBreadcrumb -Path @('Home', 'Groups', $group.displayName)

        Add-InTUIHistoryEntry -ViewType 'Group' -ViewId $GroupId -DisplayName $group.displayName

        $groupType = Get-InTUIGroupType -Group $group
        $isDynamic = $group.groupTypes -contains 'DynamicMembership'

        $propsContent = @"
[bold white]$(ConvertTo-InTUISafeMarkup -Text $group.displayName)[/]

[grey]Type:[/] $groupType
[grey]Description:[/] $($group.description ?? 'N/A')
[grey]Mail Nickname:[/] $($group.mailNickname ?? 'N/A')
[grey]Mail Enabled:[/] $($group.mailEnabled)
[grey]Security Enabled:[/] $($group.securityEnabled)
[grey]Visibility:[/] $($group.visibility ?? 'N/A')
[grey]Role Assignable:[/] $($group.isAssignableToRole ?? $false)
[grey]Created:[/] $(Format-InTUIDate -DateString $group.createdDateTime)
[grey]Renewed:[/] $(Format-InTUIDate -DateString $group.renewedDateTime)
"@


        if ($isDynamic) {
            $propsContent += @"

[grey]Membership Rule:[/] $($group.membershipRule ?? 'N/A')
[grey]Rule Processing State:[/] $($group.membershipRuleProcessingState ?? 'N/A')
"@

        }

        Show-InTUIPanel -Title "[cyan]Group Properties[/]" -Content $propsContent -BorderColor Cyan1

        $memberCountData = Show-InTUILoading -Title "[cyan]Loading member count...[/]" -ScriptBlock {
            Invoke-InTUIGraphRequest -Uri "/groups/$GroupId/members?`$top=1&`$select=id&`$count=true" -Headers @{ 'ConsistencyLevel' = 'eventual' }
        }

        if ($null -ne $memberCountData) {
            $count = $memberCountData.'@odata.count' ?? @($memberCountData.value).Count
            Write-InTUIText "[grey]Members:[/] [white]$count[/]"
            Write-InTUIText ""
        }

        $actionChoices = @(
            'View Members',
            'View Owners',
            'View Device Members'
        )

        if ($isDynamic) {
            $actionChoices += 'View Membership Rule'
        }

        $actionChoices += '─────────────'
        $actionChoices += 'Back to Groups'

        $action = Show-InTUIMenu -Title "[cyan]Group Actions[/]" -Choices $actionChoices

        Write-InTUILog -Message "Group detail action" -Context @{ GroupId = $GroupId; GroupName = $group.displayName; Action = $action }

        switch ($action) {
            'View Members' {
                Show-InTUIGroupMembers -GroupId $GroupId -GroupName $group.displayName
            }
            'View Owners' {
                Show-InTUIGroupOwners -GroupId $GroupId -GroupName $group.displayName
            }
            'View Device Members' {
                Show-InTUIGroupDeviceMembers -GroupId $GroupId -GroupName $group.displayName
            }
            'View Membership Rule' {
                Clear-Host
                Show-InTUIHeader
                Show-InTUIBreadcrumb -Path @('Home', 'Groups', $group.displayName, 'Membership Rule')
                Show-InTUIPanel -Title "Dynamic Membership Rule" -Content "[cyan]$($group.membershipRule ?? 'No rule defined')[/]" -BorderColor Cyan11
                Write-InTUIText "[grey]Processing State:[/] $($group.membershipRuleProcessingState ?? 'N/A')"
                Read-InTUIKey
            }
            'Back to Groups' {
                $exitDetail = $true
            }
            default {
                continue
            }
        }
    }
}

function Show-InTUIGroupMembers {
    <#
    .SYNOPSIS
        Shows group members.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$GroupId,

        [Parameter()]
        [string]$GroupName
    )

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path @('Home', 'Groups', $GroupName, 'Members')

    $members = Show-InTUILoading -Title "[cyan]Loading members...[/]" -ScriptBlock {
        Get-InTUIPagedResults -Uri "/groups/$GroupId/members" -PageSize 50 -Select 'id,displayName,userPrincipalName,mail,jobTitle'
    }

    if ($null -eq $members -or $members.Results.Count -eq 0) {
        Show-InTUIWarning "No members found in this group."
        Read-InTUIKey
        return
    }

    $rows = @()
    foreach ($member in $members.Results) {
        $memberType = switch ($member.'@odata.type') {
            '#microsoft.graph.user'            { '[blue]User[/]' }
            '#microsoft.graph.device'          { '[green]Device[/]' }
            '#microsoft.graph.group'           { '[cyan]Group[/]' }
            '#microsoft.graph.servicePrincipal' { '[yellow]Service Principal[/]' }
            default { ($member.'@odata.type' -replace '#microsoft\.graph\.', '') }
        }

        $rows += , @(
            $member.displayName,
            $memberType,
            ($member.userPrincipalName ?? ($member.mail ?? 'N/A')),
            ($member.jobTitle ?? 'N/A')
        )
    }

    Show-InTUITable -Title "Members of $GroupName" -Columns @('Name', 'Type', 'UPN/Email', 'Title') -Rows $rows

    # Build selectable member list (users and devices)
    $selectableMembers = @($members.Results | Where-Object {
        $_.'@odata.type' -eq '#microsoft.graph.user' -or $_.'@odata.type' -eq '#microsoft.graph.device'
    })

    if ($selectableMembers.Count -gt 0) {
        $memberChoices = @()
        foreach ($m in $selectableMembers) {
            $typeLabel = if ($m.'@odata.type' -eq '#microsoft.graph.user') { 'User' } else { 'Device' }
            $memberChoices += "[white]$(ConvertTo-InTUISafeMarkup -Text $m.displayName)[/] [grey]| $typeLabel[/]"
        }
        $choiceMap = Get-InTUIChoiceMap -Choices $memberChoices
        $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back')

        $selection = Show-InTUIMenu -Title "[cyan]View member details[/]" -Choices $menuChoices
        if ($selection -ne 'Back' -and $selection -ne '─────────────') {
            $idx = $choiceMap.IndexMap[$selection]
            if ($null -ne $idx -and $idx -lt $selectableMembers.Count) {
                $selected = $selectableMembers[$idx]
                if ($selected.'@odata.type' -eq '#microsoft.graph.user') {
                    Show-InTUIUserDetail -UserId $selected.id
                }
                elseif ($selected.'@odata.type' -eq '#microsoft.graph.device') {
                    # Resolve managed device ID from AAD device
                    $managedDevices = Invoke-InTUIGraphRequest -Uri "/deviceManagement/managedDevices?`$filter=azureADDeviceId eq '$($selected.id)'&`$select=id" -Beta
                    if ($managedDevices.value -and @($managedDevices.value).Count -gt 0) {
                        Show-InTUIDeviceDetail -DeviceId @($managedDevices.value)[0].id
                    }
                    else {
                        Show-InTUIWarning "Could not find a managed device for this AAD device."
                        Read-InTUIKey
                    }
                }
            }
        }
    }
    else {
        Read-InTUIKey
    }
}

function Show-InTUIGroupOwners {
    <#
    .SYNOPSIS
        Shows group owners.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$GroupId,

        [Parameter()]
        [string]$GroupName
    )

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path @('Home', 'Groups', $GroupName, 'Owners')

    $owners = Show-InTUILoading -Title "[cyan]Loading owners...[/]" -ScriptBlock {
        Invoke-InTUIGraphRequest -Uri "/groups/$GroupId/owners?`$select=id,displayName,userPrincipalName,mail"
    }

    if (-not $owners.value) {
        Show-InTUIWarning "No owners found for this group."
        Read-InTUIKey
        return
    }

    $rows = @()
    foreach ($owner in $owners.value) {
        $rows += , @(
            $owner.displayName,
            ($owner.userPrincipalName ?? 'N/A'),
            ($owner.mail ?? 'N/A')
        )
    }

    Show-InTUITable -Title "Owners of $GroupName" -Columns @('Name', 'UPN', 'Email') -Rows $rows
    Read-InTUIKey
}

function Show-InTUIGroupDeviceMembers {
    <#
    .SYNOPSIS
        Shows device members of a group.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$GroupId,

        [Parameter()]
        [string]$GroupName
    )

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path @('Home', 'Groups', $GroupName, 'Device Members')

    $members = Show-InTUILoading -Title "[cyan]Loading device members...[/]" -ScriptBlock {
        Get-InTUIPagedResults -Uri "/groups/$GroupId/members/microsoft.graph.device" -PageSize 50 -Select 'id,displayName,operatingSystem,operatingSystemVersion,trustType,isManaged'
    }

    if ($null -eq $members -or $members.Results.Count -eq 0) {
        Show-InTUIWarning "No device members found in this group."
        Read-InTUIKey
        return
    }

    $rows = @()
    foreach ($device in $members.Results) {
        $managed = if ($device.isManaged) { '[green]Yes[/]' } else { '[grey]No[/]' }
        $icon = Get-InTUIDeviceIcon -OperatingSystem $device.operatingSystem

        $rows += , @(
            "$icon $(ConvertTo-InTUISafeMarkup -Text $device.displayName)",
            ($device.operatingSystem ?? 'N/A'),
            ($device.operatingSystemVersion ?? 'N/A'),
            ($device.trustType ?? 'N/A'),
            $managed
        )
    }

    Show-InTUITable -Title "Device Members of $GroupName" -Columns @('Device', 'OS', 'Version', 'Trust Type', 'Managed') -Rows $rows
    Read-InTUIKey
}